【NOTE】状态压缩动态规划
状压 \(\text{DP}\)
本质上可能全是指数级暴力。(
分这么多类可能是为了方便我记住自己写过什么题,参考性不大,请大家别看。(((
技巧1:\(\color{orange}{处理全排列问题}\)
把阶乘级别问题转为指数级别问题。
研究 \(n\) 个元素的全排列时,可以考虑 \(n-1\) 个元素的全排列,再讨论最后一个元素放啥。
把 \(n\) 个元素的选择情况状压为 \(S\),那么 \(S\) 由 \(S\) 减去一个元素的子集递推而来(也就是其中 \(n-1\) 个元素全排列,减去的元素即为最后的元素)
子技巧:\(\color{pink}{枚举子集(1):只包含一个元素的子集}\)
实现方式是用 \(lowbit(x)=x\&(-x)\),得到 \(x\) 的最低 \(1\) 位对应值。
Code
P=S;
for (int I=lowbit(P);P;I=lowbit(P))
{
P-=I; T=S^I;
//...
}
例题:[SCOI2007] 排列
求数字串 \(s\) 的所有排列中能被 \(p\) 整除的个数。\(|s| \leq 10\)
朴素阶乘级做法即 \(O(|s|\times|s|!)\)。跑不过去得(
考虑阶乘级转指数级。
把 \(|s|\) 个元素的选择情况状压。该状态则由其减少一个元素的状态递推而来,而这单个元素加在最高位(最低位也可以)。根据转移的需要,再加一维表示 \(\text{mod}\ d\) 的余数。
由于会有重复的数字,还需要将最终的答案除以 \(\prod\limits_{i=0}^9 cnt[i]!\)
状态转移方程:
Code - [SCOI2007] 排列
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 12
#define MAXP 1007
int n,d;
string ss;
inline int b(int A) { return 1<<(A-1); }
inline int lowbit(int A) { return A&(-A); }
int f[1<<MAXN][MAXP];
int a[1<<MAXN];
int cnt[1<<MAXN];
long long powt[MAXN],frac[MAXN];
int CNT[MAXN];
inline void R()
{//INIT!
cin>>ss; scanf("%d",&d);
n=ss.size();
for (int i=0;i<=9;i++) CNT[i]=0;
for (int i=1;i<=n;i++) a[b(i)]=(int)(ss[i-1]-'0'),CNT[(int)(ss[i-1]-'0')]++;
int E=b(n+1)-1; f[0][0]=1; cnt[0]=0;
int P,T;
for (int S=1;S<=E;S++)
{
for (int i=0;i<d;i++) f[S][i]=0;
P=S; cnt[S]=cnt[S^lowbit(S)]+1;
for (int I=lowbit(P);P;I=lowbit(P))
{
P-=I; T=S^I;
for (int i=0;i<d;i++)
f[S][(1ll*i+1ll*a[I]*powt[cnt[T]]%d)%d]+=f[T][i];
}
}
long long mu=1;
for (int i=0;i<=9;i++) mu*=frac[CNT[i]];
printf("%lld\n",f[E][0]/mu);
return;
}
int main()
{
powt[0]=frac[0]=1;
for (int i=1;i<=10;i++) powt[i]=powt[i-1]*10,frac[i]=frac[i-1]*i;
int T;
scanf("%d",&T);
while (T--) R();
return 0;
}
类似题目:yyy loves Maths VII
- 注意:lowbit 优化下,该算法的复杂度为 \(\sum\limits_{i=0}^ni{n \choose i}=n2^{n-1}\)
子技巧:\(\color{pink}{匹配问题(1):前缀的排列匹配}\)
某种一一匹配问题,转移和匹配位置/匹配对象有关之类的。也是采用状压排列,排列内的数对应匹配对象的前缀。
Atcoder好像做到过一道很典的,等会我找找。之前写这个的时候陷入过一个误区,不需要一次性把影响的位置答案算出,只需要算当前集合直接相关的答案即可。
例题:邦邦的大合唱站队
设 \(f[S]\) 表示集合 \(S\) 内的乐队全部放到最前,\(S\) 内乐队需要出队的人数。
Code - 邦邦的大合唱站队
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN (int)(1e5+233)
#define MAXM 22
int a[MAXN];
int f[1<<MAXM],sum[1<<MAXM],as[MAXM];
int n,m;
int lsum[MAXN][MAXM];
inline int b(int A) { return (1<<(A-1)); }
inline int lowbit(int A) { return (A&(-A)); }
inline int cnt(int A) { int sum=0; while (A) sum++,A>>=1; return sum; }
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++) scanf("%d",&a[i]),as[a[i]]++;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=m;j++)
lsum[i][j]=lsum[i-1][j];
lsum[i][a[i]]++;
}
int E=b(m+1)-1;
for (int S=1;S<=E;S++) sum[S]=sum[S^lowbit(S)]+as[cnt(lowbit(S))],f[S]=1e9;
f[0]=0;
for (int S=1,P;S<=E;S++)
{
P=S;
for (int i=lowbit(P),l,r,d;P;i=lowbit(P))
{
P^=i; d=cnt(i);
l=sum[S^i]; r=sum[S];
f[S]=min(f[S],f[S^b(d)]+lsum[l][d]+lsum[n][d]-lsum[r][d]);
}
}
printf("%d\n",f[E]);
return 0;
}
技巧2:\(\color{orange}{处理选择具有相对限制的问题}\)
选择的相对限制,比如最经典的不能相邻。
用状压可以很好的枚举和判断合法的情况。
例题:[SCOI2005] 互不侵犯
选择的位置周围八个格子不能被选择。
一行一行填的话,会发现当前行的限制只与上一行有关。于是设 \(f[i][S][k]\) 表示填了前 \(i\) 行,第 \(i\) 行 状态为 \(S\),共摆了 \(k\) 个的方案数
很暴力的题目。
Code - [SCOI2005] 互不侵犯
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 10
#define MAXK 85
//f[S][i]
long long f[MAXN][1<<MAXN][27];
inline int b(int A) { return 1<<(A-1); }
int pcnt[1<<MAXN];
inline int lowbit(int A) { return A&(-A); }
inline void printbit(int A) { for (int i=1;i<=3;i++) printf("%d",A&1),A>>=1; return; }
int main()
{
int n,k;
scanf("%d%d",&n,&k);
if (k*4>((n&1)?n+1:n)*((n&1)?n+1:n)) { puts("0"); return 0; }
f[0][0][0]=1;
int E=b(n+1)-1;
for (int S=1;S<=E;S++) pcnt[S]=pcnt[S^lowbit(S)]+1;
for (int i=1;i<=n;i++)
for (int S=0;S<=E;S++)
{
if ((S&(S<<1))||(S&(S>>1))) continue;
for (int T=0;T<=E;T++)
{
if ((T&(T<<1))||(T&(T>>1))) continue;
if ((S&T)||((S<<1)&T)||((S>>1)&T)) continue;
for (int c=0;c<=k;c++)
if (c+pcnt[T]>k) break;
else f[i][T][c+pcnt[T]]+=f[i-1][S][c];
}
}
long long ans=0;
for (int S=0;S<=E;S++) ans+=f[n][S][k];//,printf("f[%d][",n),printbit(S),printf("][%d]=%lld\n",k,f[n][S][k]);
printf("%lld\n",ans);
return 0;
}
子技巧:\(\color{pink}{判断是否达成相对限制的冲突}\)
比如上面这个题就是判 \((S<<1) \And T\),\((S>>1) \And T\),\(S \And T\) 来判断相邻行状态 \(S\) 与 \(T\) 是否冲突。
\((S>>1) \And S\) 来判断行状态为 \(S\) 自己是否冲突。
子技巧:\(\color{pink}{节省无用状态}\)
例题:[NOI2001] 炮兵阵地
同样是有限制,当前行选择限制与上两行有关。所以状态里面需要压两行,而单行转移复杂度就高达了 \(\text{O(} 2^{3m} \text{)}\)
但是单行也是有限制的。符合限制的状态个数,打表后得到,只有 \(60\) 个。我把它们打表薄纱了(
设 \(f[i][IS][IN]\) 表示前 \(i\) 行,第 \(i\) 行为第 \(IN\) 种状态,第 \(i-1\) 行为第 \(IS\) 种状态,最大放置数。转移方程即
降到 \(\text{O(}60^3n \text{)}\)
Code - [NOI2001] 炮兵阵地
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 107//!
#define MAXM 12
inline int b(int A) { if (A<0) return 0; return 1<<(A-1); }
//inline int countbit(int A) { int sum=0; while (A) sum+=A&1,A>>=1; return sum; }
//inline void printbit(int A) { printf(" "); for (int i=1;i<=4;i++) printf("%d",(int)((A&b(i))!=0)); printf(" "); return; }
/*
inline bool check(int S)
{
for (int i=1;i<=10;i++)
if ((S&b(i))&&((S&b(i-1))||(S&b(i-2))||(S&b(i+1))||(S&b(i+2)))) return false;
return true;
}
*/
//2,3,4,6
int a[100]={0,0,1,2,4,8,9,16,17,18,32,33,34,36,64,65,66,68,72,73,128,129,130,132,136,137,144,145 \
,146,256,257,258,260,264,265,272,273,274,288,289,290,292,512,513,514,516,520,521 \
,528,529,530,544,545,546,548,576,577,578,580,584,585};
int tota[MAXM]={0,2,3,4,6,9,13,19,28,41,60};
int bcnt[100]={0,0,1,1,1,1,2,1,2,2,1,2,2,2,1,2,2,2,2,3,1,2,2,2,2,3,2,3,3,1,2,2,2,2,3,2,3,3,2,3,3, \
3,1,2,2,2,2,3,2,3,3,2,3,3,3,2,3,3,3,3,4};
int f[MAXN][62][62];
//int f[61][61][210];//我的评价是,不如退役(Runtime Error)
/*
(N&S)||(N&P) continue;
f[i][S][N]=max(f[i][S][N],f[i-1][P][S]+bcnt[N])
f[1][0][S]=bcnt[S]
*/
int sta[MAXN];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
// int E=b(m+1)-1;
// int sum=0;
// int Pp=1; for (int S=0;S<=E+1;S++) { if (check(S)) sum++; if (S==Pp-1) printf("%d,",sum),Pp<<=1; }
// for (int i=1;i<=tota;i++) printf("%d,",countbit(a[i]));
char C;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
{
cin>>C;
if (C=='H') sta[i]|=b(j);
}
// for (int i=1;i<=60;i++) printbit(a[i]),puts("");
int ans=0;
for (int i=1;i<=tota[m];i++)
{
if (!(a[i]&sta[1]))
f[1][1][i]=bcnt[i],ans=max(ans,bcnt[i]);
// printf("f[1][0]["); printbit(a[i]); printf("]=%d=f[%d][%d][%d]\n",f[1][0][i],1,0,i);
}
int P,S,N;
for (int i=2;i<=n;i++)
{
for (int IP=1;IP<=tota[m];IP++)
{
P=a[IP];
if (P&sta[i-2]) continue;
for (int IS=1;IS<=tota[m];IS++)
{
S=a[IS];
if (P&S) continue;
if (S&sta[i-1]) continue;
for (int IN=1;IN<=tota[m];IN++)
{
N=a[IN];
if ((N&S)||(N&P)) continue;
if (N&sta[i]) continue;
f[i][IS][IN]=max(f[i][IS][IN],f[i-1][IP][IS]+bcnt[IN]);
// if (N==2&&S==9&&P==0) { printf("%d",i-1); printbit(P); printbit(S); printf("%d %d+%d also = f[%d][%d][%d]\n",f[i][IS][IN],f[i-1][IP][IS],bcnt[IN],i-1,IP,IS); system("pause"); }
ans=max(ans,f[i][IS][IN]);
// printf("f[%d][",i); printbit(S); printf("]["); printbit(N); printf("]=%d\n",f[i][IS][IN]);
}
}
}
}
printf("%d\n",ans);
return 0;
}
技巧3:\(\color{orange}{处理状态转移只与左侧,左上位置有关的问题(轮廓线\text{DP})}\)
或许也能处理与转移与整条轮廓相关的,但我题量太少了没见过。
子技巧:\(\color{pink}{两行状态\longrightarrow一行状态}\)
弱化版轮廓线,但是 \(k\) 进制(
例题:Luogu P2435 染色
这题按顺序枚举填色位置即可。只需要考虑这个点左侧,左上位置的颜色,那么我们需要的轮廓线如下图红色部分:

然后爆转移就好了。
Code - P2435 染色
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 107
#define MAXM 15
#define MMMM (int)(1e5+233)
const int mod=376544743;
int n,m,k;
int f[2][MAXM][65537];
int P[MAXM],Q[MAXM],A[MMMM],B[MMMM];
inline void printKBIT(int S)
{
int B[10];
printf("printBIT: ");
for (int i=1;i<=m;i++) B[i]=S%k,S/=k;
for (int i=m;i>=1;i--) printf("%d ",B[i]);
puts("");
return;
}
void dfs(int I,int J,int x)
{
// puts("?");
if (x==m+1)
{
int S=0;
for (int i=1;i<=m;i++) S=S*k+P[i],Q[i]=P[i];
// printf("===================================(%d,%d) ",I,J); printKBIT(S);
// puts(""); for (int i=1;i<=m;i++) printf("%d ",P[i]); puts("");
// printf("ERRRRRRRRRRRRRRRRRRRIN: \n"); printKBIT(S);
for (int i=0;i<k;i++)
if (i!=P[J]&&i!=P[J+1])
{
int S2=0;
Q[J+1]=i;
for (int j=1;j<=m;j++) S2=S2*k+Q[j];
// printf("-------\n");
// printf(">>>>>>>>>> (%d,%d)\n",I,J);
// printf("%d ",f[I][J][S]); printKBIT(S); printf("to\n"); printKBIT(S2);
// printf("-------\n");
f[I][J+1][S2]=(f[I][J+1][S2]+f[I][J][S])%mod;
}
return;
}
if (x==J+1)
{
for (int i=0;i<k;i++)
{
P[x]=i;
dfs(I,J,x+1);
}
}
else
{
for (int i=0;i<k;i++)
if (P[x-1]==i) continue;
else P[x]=i,dfs(I,J,x+1);
}
}
inline void sol(int I,int J)
{
if (J==m)
{
int RG=pow(k,m+1); //cout<<"{"<<RG<<endl; system("pause");
for (int S=0;S<RG;S++)
{
f[I^1][0][S]=f[I][J][S];
for (int j=1;j<=m;j++)
f[I^1][j][S]=0;
}
return;
}
dfs(I,J,1);
}
//f[i][j][S1]->f[i][j+1][S2]
int main()
{
scanf("%d%d%d",&n,&m,&k);
int S0=0,Sr=0;
for (int i=1;i<=m;i++) scanf("%d",&A[i]),S0=S0*k+A[i];
for (int i=1;i<=m;i++) scanf("%d",&B[i]),Sr=Sr*k+B[i];
if (n>100)
{
for (int i=1;i<=m;i++)
if (A[i]==(B[i]^(n&1)))
return puts("0"),0;
puts("1");
return 0;
}
P[0]=A[0]=P[m+1]=A[m+1]=-1;
f[2&1][0][S0]=1;
// printKBIT(S0);
for (int i=2;i<=n;i++)
{
for (int j=0;j<=m;j++)
{
sol(i&1,j);
}
}
// printf("+%d\n",f[2][2][7]);
printf("%d\n",f[n&1][m][Sr]);
return 0;
}
同样的,这个方法也可以来优化上面那道 互不侵犯。
用一个 \(01\) 串来表示这个已处理状态的轮廓线:从矩阵的右上角开始,轮廓线向下为 1,向左为 0,组成长度为 \(n+m\) 的一个 \(01\) 串。
例题:「九省联考 2018」一双木棋
一个格子可以落子当且仅当这个格子内没有棋子,且这个格子的 左侧 及 上方 的所有格子内都有棋子。
正好是满足这个限制的。
举个例子:

这个 $ 3\times 4$ 的矩阵中,蓝色部分是已落子的话,红色就是对应的轮廓线,表示为 \(\text{0101001}\) 。
每个落子局面都对应了一条轮廓线,然后状态就有了(
初始状态形如 \(\text{00000} \cdots \text{11111}\),完成状态形如 \(\text{11111} \cdots \text{00000}\)
剩下的部分就是博弈 \(\text{DP}\) 了。稍微放一下做法
两个人都在自己的回合选择最优的策略,也就是:当他知道接下来的一步下在每个地方分别对应的分数,他就会选择其中分数最高的一个。
做法也就是逆推状态。
设 \(f_{S}\) 表示轮廓线 \(S\) 到结束状态的最优决策分数。这个决策分数定义为 \(a_i\) 的总和减去 \(b_i\) 的总和。
对于这个先手妹,就是要在 \(f_S\) 的扩展中选一个最大的;对于这个后手妹,就是要在 \(f_S\) 中的扩展中选一个最小的。
这样初始状态就变为了轮廓线的最终状态,答案就变成了轮廓线初始状态的答案。
另外就是,可扩展的位置是相邻的 01,记得要判一下(
Code - 「九省联考 2018」一双木棋
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 12
int n,m;
inline int bi(int S,int I) { return ((S>>(n+m-I))&1); }
int a[MAXN][MAXN],b[MAXN][MAXN];
int f[(int)(1<<22)];
bool p[(int)(1<<22)];
bool book[(int)(1<<22)];
inline int dir(int S,int I,int typ)
{
int x=n,y=1;
for (int i=1;i<=n+m-I-1;i++)
{
if (S&1) x--;
else y++;
S>>=1;
}
if (typ==0) return b[x][y];
else return a[x][y];
}
inline void printBIT(int S)
{
int B[1007];
for (int i=n+m;i;i--)
{
B[i]=S&1;
S>>=1;
}
printf("printBIT: "); for (int i=1;i<=n+m;i++) printf("%d",B[i]); puts("");
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
scanf("%d",&b[i][j]);
int MAXS=(1<<(n+m))-(1<<(m));
int MINS=(1<<(n))-1; int Sy;
for (int S=MINS;S<=MAXS;S++) f[S]=-2e9;
p[MAXS]=((n*m)&1);
book[MAXS]=1;
f[MAXS]=0;
// printBIT(MINS);
for (int S=MAXS;S>MINS;S--)
{
if (!book[S]) continue;
// printBIT(S);
for (int i=1;i<n+m;i++)
if (bi(S,i)==1&&bi(S,i+1)==0)
{
// printBIT(S>>(n+m-(i+1)));
Sy=((S^(1<<(n+m-i)))^(1<<(n+m-i-1)));
/*
printf("_________________\n");
printBIT(S);
puts("to");
printBIT(Sy);
printf("~~~~~~~~~~~~~~~~~\n");
*/
book[Sy]=1;
p[Sy]=(p[S]^1);
if (p[Sy]==1)
f[Sy]=min(f[Sy]==(int)(-2e9)?(int)(2e9):f[Sy],f[S]-dir(S,i,0));
else f[Sy]=max(f[Sy],f[S]+dir(S,i,1));
}
}
printf("%d\n",f[MINS]);
return 0;
}
技巧4:\(\color{orange}{处理进位}\)
这个好像不大算状压。只是稍微提一嘴可以这么写
现在做过的只有
例题:[NOIP2021] 数列
特征是加位时可以从小到大(按照一定顺序),并且需要考虑数的 \(countbit\)
这题的加位次数很小,只有 \(30\),所以只需要存相邻五位的进位。
技巧5:\(\color{orange}{处理集合相斥不重关系的分组}\)
比如说给定一系列集合,要求分成两组(多组能不能做我不知道,等会想想),要求两组并集交集为空。
设 \(f_{S}\) 表示并集为 \(S\) 的选择方案,然后枚举其补集的子集应该就可以。
子技巧:\(\color{pink}{枚举子集(2):任意子集}\)
for (int S=1;S<(1<<n);S++)
for (int j=(S-1)&S;j;j=(j-1)&S)
f[S]=max(f[S],f[j]+f[S^j]);
复杂度好像是 \(O(3^n)\) 的。
另外这种子集的信息合并,可能有的是取 \(max\) 有的是求和之类的。如果求和,并且两个集合不作区分的话,这样会算重。
下文 杂题选解 - 地震后的幻想乡 中有提到解决方法:钦定某个元素在前一个集合中,就不会重了。
例题:Educational DP Contest U - Grouping
设 \(f[S]\) 表示集合 \(S\) 的最大价值。先处理该集合分为单组的价值,然后枚举子集合并即可。
Code - Educational DP Contest U - Grouping
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
#define MAXN 17
inline int lowbit(int a) { return a&(-a); }
long long f[1<<MAXN];
int a[MAXN][MAXN];
int n;
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
for (int S=1;S<(1<<n);S++)
{
f[S]=f[S-lowbit(S)];
int M=lowbit(S),sm=0;
while (M) { sm++; M>>=1; }
M=(S>>sm); int sn=sm;
while (M)
{
sn++;
if (M&1) f[S]+=a[sm][sn];
M>>=1;
}
}
for (int S=1;S<(1<<n);S++)
{
for (int j=(S-1)&S;j;j=(j-1)&S)
{
f[S]=max(f[S],f[j]+f[S^j]);
}
}
printf("%lld\n",f[(1<<n)-1]);
return 0;
}
先处理出每个 \(S\)
原理现在反看很自然。从大到小枚举子集呢
例题:[NOI2015] 寿司晚宴
每个数分解完最多会有一个 \(>22\) 的质因子。把这个大质因子单独提出来,并且将数按其排序
处理跟刚才不大一样。先枚举相同大质因子的家伙(
设 \(g1_{S,T},g2_{S,T}\) 分别表示该质因子给了前一个集合和该质因子给了后一个集合,两集合小质因数状态为 \(S,T\) 的方案数。\(f_{S,T}\) 则表示在该段数前的方案数。
新加数的小质因数集合状态为 \(P\):
区间遍历完后
子技巧:\(\color{pink}{枚举一个集合拆分成两个子集}\)
在这两个集合没有区分的情况下,直接枚举子集和补,这东西会重(
所以我们钦定某个元素在前面一个子集中,就不会重了。
技巧6:\(\color{orange}{匹配问题(2):处理两集合元素}\)
例题:Educational DP Contest O - Matching
设 \(f_{i,S}\) 表示前一个集合的前 \(i\) 个点与后一个集合的 \(S\) 匹配的匹配数。则:
- 由于这里要求的是 \(countbit_S=i\),大可以先枚举 \(S\),再让 \(i=countbit_S\) 。
杂题选解
[HNOI2012]集合选数
感觉不套路()稍微写写
以 \(1\) 为第一个元素,其他位置元素填上其左侧元素 \(\times 2\) 的值,或者上位元素 \(\times 3\) 的值。
如:
1 2 4 8 16 32 ...
3 6 12 24 48 96 ...
9 18 36 72 ...
27 ...
一个 \(log_2 \times log_3\) 级别的矩阵,要求不选择相邻元素(技巧2),状压 \(DP\) 即可。
枚举 \(2\) 和 \(3\) 之外的因子组合。其实也不需要,直接从小到大枚举第一个没被选的数。继续这样构造矩阵,反复做上面那玩意。
复杂度 \(O(能过)\) 。不懂()QAQ(
[SCOI2008] 奖励关
期望题,倒着做就好()似乎没啥特别的。
设 \(f_{i,S}\) 表示前 \(i\) 行状态为 \(S\) ,第 \(i\) 至 \(k\) 轮的最大期望收益。
[ZJOI2015] 地震后的幻想乡
疯狂转化的一道题()
(问题可以转化为)给定一棵树,其边权是均匀随机生成的全排列,求所有情况最小生成树最大边的总和(虽然是期望,但是每种情况等概率)。
首先认识一个问题:最小生成树最大边,其实是往图中从小到大加边的全图联通戳。
问题又转化为:设 \(f_{i,S}\) 为在图 \(S\) 中选择 \(i\) 条边使得全图恰好连通的方案数。则:
还是不好做的。使用容斥(把恰好变为可行)再转一遍:设 \(g_{i,S}\) 为在图 \(S\) 中选择 \(i\) 条边使得全图连通的方案数。则:
仍然不好做()。再根据正难则反的单步容斥:设 \(h_{i,S}\) 为在图 \(S\) 中选择 \(i\) 条边使得全图不连通的方案数。
既然不连通了就可以拆两半了(?)再考虑计数的不重不漏,我们钦定拆出的第一个集合 \(T\) 是连通的。大概就会得到这样一个东西:
但其实还有个问题,由于任意连接,集合 \(S \oplus T\) 也有可能是联通的。那么钦定某个点 \(p\) 在集合 \(T\) 中,就可以解决这个问题了。
实际上这题原意来看()最后答案是
[NOIP2017 提高组] 宝藏
可能要多给自己洗脑一下 \(\text{DP}\) 的思想。听说这题搜索+剪枝是可以过的,发现自己甚至不大会写,震。
设状态的时候,我们需要知道:
-
当前这步转移所需要的信息;
-
状态的某种扩展方法,使得该 \(\text{DP}\) 能覆盖最优解。
我们需要的信息是:当前扩展点在树中的深度。所以可以尝试设一维与树高相关。
转移呢?
由于加点加越深该点代价越大,可以假装当前的扩展点全都加在原本树高 \(+1\) 这一层,而该做法可以覆盖所有合法解,并且最优解不会比不合法解劣。即每次转移树中一层的点。
预处理一个点集可以扩展的点集。扩展的时候枚举该点集的子集即可。
代价需要一个 \(n^2\) 求。
\(S\) 到 \(T\) 的最小边权和需要暴力预处理。
\(O(3^nn^2)\) 的。感觉很卡,乌乌
[yLOI2020] 凉凉
和宝藏类似,枚举路径加在的层数。
不写了。乌乌
[NOIP2016 提高组] 愤怒的小鸟
关键在于节省转移状态。
设 \(f_{S}\) 表示经过点集 \(S\) 所需的线数,\(p_{i,j}\) 表示经过 \(i,j\) 两点的点集。则:
枚举 \(i,j\) 两点好像需要 \(n^2\),数据范围难以接受
然而我们发现 某个 \(S\) 到某个 \(T\) 的状态,可能经历了多条不同线的不同顺序的转移,但结果实际上是一样的(?)
那我们就规定每次转移的时候这条线要包含当前未经过的第一个点,这样就节省了很多重复转移。节省掉了一个 \(n\) ()
AtCoder Beginner Contest 274 E - Booster
从起点开始,每经过一个宝箱速度就会 \(\times2\) 。
设 \(f[i][S][T]\) 表示现在在点 \(i\) ,经过了 \(S\) 的城镇,\(T\) 的箱子需要的最少步数
大概是 \(O(2^{n+m}(n+m)^2)\)
[SDOI2009] Bill的挑战
求刚好 \(k\) 个串匹配的串 \(T\) 个数。
法1:
首先看看如果一个串 \(T\) 和 \(k\) 个串匹配的具体表现:
\(k\) 个串,某一位上每个字符都是 \(?\) 或者某个相同字符 \(C\)
每个串集合是可枚举的(?)
好像跑一下每个集合是否有对应串也跑得过来。那样就得到和钦定 \(k\) 个串匹配(?)的串 \(T\) 个数
设 \(f_k\) 表示恰好和 \(k\) 个串匹配的串 \(T\) 个数,\(g_k\) 表示钦定了 \(k\) 个串匹配的串 \(T\) 个数。
则:
法2:
还有转移的时候取交集这种设定啊...
设 \(f_{i,S}\) 表示当前匹配到了第 \(i\) 个字符,匹配状态集合为 \(S\) 的字符串 \(T\) 个数,\(p_{i,c}\) 表示 第 \(i\) 位为字符 \(c\) 匹配的串集合。则:
[SDOI2009] 学校食堂
设 \(f_{i,S}\) 表示前 \(i\) 个人,第 \(i\sim i+6\) 个人已经打饭的为 \(S\) 的时间。
由于需要计算相邻代价,还要加一维当前最后一个打饭的人是谁
设 \(f_{i,j,S}\) 表示前 \(i\) 个人,第 \(i\sim i+6\) 个人已经打饭的为 \(S\),最后一个打饭的人为 \(j_S\) 的最小时间。
伟大的chy曾经说过:状压就是个暴搜啊
乌乌,他说的对,我不写了/dk
END.

浙公网安备 33010602011771号