【NOTE】状态压缩动态规划

状压 DP

本质上可能全是指数级暴力。(

分这么多类可能是为了方便我记住自己写过什么题,参考性不大,请大家别看。(((

目录

技巧1:

把阶乘级别问题转为指数级别问题。

研究 n 个元素的全排列时,可以考虑 n1 个元素的全排列,再讨论最后一个元素放啥。

n 个元素的选择情况状压为 S,那么 SS 减去一个元素的子集递推而来(也就是其中 n1 个元素全排列,减去的元素即为最后的元素)

子技巧:(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|10

朴素阶乘级做法即 O(|s|×|s|!)。跑不过去得(

考虑阶乘级转指数级。

|s| 个元素的选择情况状压。该状态则由其减少一个元素的状态递推而来,而这单个元素加在最高位(最低位也可以)。根据转移的需要,再加一维表示 mod d 的余数。

由于会有重复的数字,还需要将最终的答案除以 i=09cnt[i]!

状态转移方程:

f[S][(k+a[i]×10countbit[T] mod d) mod d]+=f[T][k]

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 优化下,该算法的复杂度为 i=0ni(ni)=n2n1

子技巧:(1)

某种一一匹配问题,转移和匹配位置/匹配对象有关之类的。也是采用状压排列,排列内的数对应匹配对象的前缀。

Atcoder好像做到过一道很典的,等会我找找。之前写这个的时候陷入过一个误区,不需要一次性把影响的位置答案算出,只需要算当前集合直接相关的答案即可。

例题:邦邦的大合唱站队

f[S] 表示集合 S 内的乐队全部放到最前,S 内乐队需要出队的人数。

l=sum[S(1<<i)],r=sum[S]f[S]=f[S(1<<i)+lsum[l]+rsum[r+1]]f[S]=f[S(1<<i)+lsum[l]+lsum[n]lsum[r]

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:

选择的相对限制,比如最经典的不能相邻。

用状压可以很好的枚举和判断合法的情况。

例题:[SCOI2005] 互不侵犯

选择的位置周围八个格子不能被选择。

一行一行填的话,会发现当前行的限制只与上一行有关。于是设 f[i][S][k] 表示填了前 i 行,第 i 行 状态为 S,共摆了 k 个的方案数

f[i][T][c+countbit[T]]+=f[i1][S][c]

很暴力的题目。

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;
}

子技巧:

比如上面这个题就是判 (S<<1)&T(S>>1)&TS&T 来判断相邻行状态 ST 是否冲突。

(S>>1)&S 来判断行状态为 S 自己是否冲突。

子技巧:

例题:[NOI2001] 炮兵阵地

同样是有限制,当前行选择限制与上两行有关。所以状态里面需要压两行,而单行转移复杂度就高达了 O(23m)

但是单行也是有限制的。符合限制的状态个数,打表后得到,只有 60 个。我把它们打表薄纱了(

f[i][IS][IN] 表示前 i 行,第 i 行为第 IN 种状态,第 i1 行为第 IS 种状态,最大放置数。转移方程即

f[i][IS][IN]=max(f[i][IS][IN],f[i1][IP][IS]+countbit[IN])

降到 O(603n)

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:线DP

或许也能处理与转移与整条轮廓相关的,但我题量太少了没见过。

子技巧

弱化版轮廓线,但是 k 进制(

例题:Luogu P2435 染色

这题按顺序枚举填色位置即可。只需要考虑这个点左侧,左上位置的颜色,那么我们需要的轮廓线如下图红色部分:

image

然后爆转移就好了。

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」一双木棋

一个格子可以落子当且仅当这个格子内没有棋子,且这个格子的 左侧 及 上方 的所有格子内都有棋子。

正好是满足这个限制的。

举个例子:

image

这个 3×4 的矩阵中,蓝色部分是已落子的话,红色就是对应的轮廓线,表示为 0101001

每个落子局面都对应了一条轮廓线,然后状态就有了(

初始状态形如 0000011111,完成状态形如 1111100000

剩下的部分就是博弈 DP 了。稍微放一下做法

两个人都在自己的回合选择最优的策略,也就是:当他知道接下来的一步下在每个地方分别对应的分数,他就会选择其中分数最高的一个。

做法也就是逆推状态。

fS 表示轮廓线 S 到结束状态的最优决策分数。这个决策分数定义为 ai 的总和减去 bi 的总和。

对于这个先手妹,就是要在 fS 的扩展中选一个最大的;对于这个后手,就是要在 fS 中的扩展中选一个最小的。

这样初始状态就变为了轮廓线的最终状态,答案就变成了轮廓线初始状态的答案。

另外就是,可扩展的位置是相邻的 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:

这个好像不大算状压。只是稍微提一嘴可以这么写

现在做过的只有

例题:[NOIP2021] 数列

特征是加位时可以从小到大(按照一定顺序),并且需要考虑数的 countbit

这题的加位次数很小,只有 30,所以只需要存相邻五位的进位。


技巧5:

比如说给定一系列集合,要求分成两组(多组能不能做我不知道,等会想想),要求两组并集交集为空。

fS 表示并集为 S 的选择方案,然后枚举其补集的子集应该就可以。

子技巧:(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(3n) 的。

另外这种子集的信息合并,可能有的是取 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 的质因子。把这个大质因子单独提出来,并且将数按其排序

处理跟刚才不大一样。先枚举相同大质因子的家伙(

g1S,T,g2S,T 分别表示该质因子给了前一个集合和该质因子给了后一个集合,两集合小质因数状态为 S,T 的方案数。fS,T 则表示在该段数前的方案数。

新加数的小质因数集合状态为 P

g1S|P,T+=g1S,T

g2S,T|P+=g2S,T

区间遍历完后

fS,T=g1S,T+g2S,TfS,T

g1S,T=g2S,T=fS,T

子技巧:

在这两个集合没有区分的情况下,直接枚举子集和补,这东西会重(

所以我们钦定某个元素在前面一个子集中,就不会重了。


技巧6:(2)

例题:Educational DP Contest O - Matching

fi,S 表示前一个集合的前 i 个点与后一个集合的 S 匹配的匹配数。则:

fi,S=j=1nfi1,S(1<<(j1))   (ai,j=1,S&(1<<(j1))!=0)

  • 由于这里要求的是 countbitS=i,大可以先枚举 S,再让 i=countbitS

杂题选解

[HNOI2012]集合选数

感觉不套路()稍微写写

1 为第一个元素,其他位置元素填上其左侧元素 ×2 的值,或者上位元素 ×3 的值。

如:

1  2  4  8   16  32  ...
3  6  12 24  48  96  ...
9  18 36 72  ...
27 ...

一个 log2×log3 级别的矩阵,要求不选择相邻元素(技巧2),状压 DP 即可。

枚举 23 之外的因子组合。其实也不需要,直接从小到大枚举第一个没被选的数。继续这样构造矩阵,反复做上面那玩意。

复杂度 O() 。不懂()QAQ(


[SCOI2008] 奖励关

期望题,倒着做就好()似乎没啥特别的。

fi,S 表示前 i 行状态为 S ,第 ik 轮的最大期望收益。

fi,S=1np=1nmax(fi+1,S|(1<<(p1))+[S&T[P]=T[P]]×ap,fi+1,S)


[ZJOI2015] 地震后的幻想乡

疯狂转化的一道题()

(问题可以转化为)给定一棵树,其边权是均匀随机生成的全排列,求所有情况最小生成树最大边的总和(虽然是期望,但是每种情况等概率)。

首先认识一个问题:最小生成树最大边,其实是往图中从小到大加边的全图联通戳。

问题又转化为:设 fi,S 为在图 S 中选择 i 条边使得全图恰好连通的方案数。则:

ans=i=1mi×fi,SFULL

还是不好做的。使用容斥(把恰好变为可行)再转一遍:设 gi,S 为在图 S 中选择 i 条边使得全图连通的方案数。则:

fi,S=gi,Sgi1,S

仍然不好做()。再根据正难则反的单步容斥:设 hi,S 为在图 S 中选择 i 条边使得全图不连通的方案数。

gi,S=(sumedgeSi)hi,S

既然不连通了就可以拆两半了(?)再考虑计数的不重不漏,我们钦定拆出的第一个集合 T 是连通的。大概就会得到这样一个东西:

hi,S=j=0igj,T×(hij,ST+hij,ST)=j=0igj,T×(sumedgeSTij)

但其实还有个问题,由于任意连接,集合 ST 也有可能是联通的。那么钦定某个点 p 在集合 T,就可以解决这个问题了。

实际上这题原意来看()最后答案是

ans=1mi=1mi×fi,SFULL


[NOIP2017 提高组] 宝藏

可能要多给自己洗脑一下 DP 的思想。听说这题搜索+剪枝是可以过的,发现自己甚至不大会写,震。

设状态的时候,我们需要知道:

  1. 当前这步转移所需要的信息;

  2. 状态的某种扩展方法,使得该 DP 能覆盖最优解。

我们需要的信息是:当前扩展点在树中的深度。所以可以尝试设一维与树高相关。

转移呢?

由于加点加越深该点代价越大,可以假装当前的扩展点全都加在原本树高 +1 这一层,而该做法可以覆盖所有合法解,并且最优解不会比不合法解劣。即每次转移树中一层的点。

预处理一个点集可以扩展的点集。扩展的时候枚举该点集的子集即可。

代价需要一个 n2 求。

fi,S|T=min{fi1,S+(i1)×asum[S][T]}

ST 的最小边权和需要暴力预处理。

O(3nn2) 的。感觉很卡,乌乌


[yLOI2020] 凉凉

和宝藏类似,枚举路径加在的层数。

不写了。乌乌


[NOIP2016 提高组] 愤怒的小鸟

关键在于节省转移状态。

fS 表示经过点集 S 所需的线数,pi,j 表示经过 i,j 两点的点集。则:

fS|pi,j=min(fS+1)fS|(1<<(i1))=min(fS+1)

枚举 i,j 两点好像需要 n2,数据范围难以接受

然而我们发现 某个 S 到某个 T 的状态,可能经历了多条不同线的不同顺序的转移,但结果实际上是一样的(?)

那我们就规定每次转移的时候这条线要包含当前未经过的第一个点,这样就节省了很多重复转移。节省掉了一个 n ()


AtCoder Beginner Contest 274 E - Booster

从起点开始,每经过一个宝箱速度就会 ×2

f[i][S][T] 表示现在在点 i ,经过了 S 的城镇,T 的箱子需要的最少步数

f[j][S(|(1<<(j1)))][T(|(1<<(j1)))]=min{f[i][S][T]+dis(i,j)2countbit[T]}

大概是 O(2n+m(n+m)2)


[SDOI2009] Bill的挑战

求刚好 k 个串匹配的串 T 个数。

法1:

首先看看如果一个串 Tk 个串匹配的具体表现:

k 个串,某一位上每个字符都是 ? 或者某个相同字符 C

每个串集合是可枚举的(?)

好像跑一下每个集合是否有对应串也跑得过来。那样就得到和钦定 k 个串匹配(?)的串 T 个数

fk 表示恰好和 k 个串匹配的串 T 个数,gk 表示钦定了 k 个串匹配的串 T 个数。

gk=i=kn(ik)fi

则:

fk=i=kn(1)ik(ik)gi

法2:

还有转移的时候取交集这种设定啊...

fi,S 表示当前匹配到了第 i 个字符,匹配状态集合为 S 的字符串 T 个数,pi,c 表示 第 i 位为字符 c 匹配的串集合。则:

fi,Spi,cfi,S


[SDOI2009] 学校食堂

fi,S 表示前 i 个人,第 ii+6 个人已经打饭的为 S 的时间。

由于需要计算相邻代价,还要加一维当前最后一个打饭的人是谁

fi,j,S 表示前 i 个人,第 ii+6 个人已经打饭的为 S,最后一个打饭的人为 jS 的最小时间。

fi,j,S=fi1,j+1,S>>1(j7)fi,k,S|b(k)=min{fi,j,S+(Ti+jTk)}pS,kp+Bp


伟大的chy曾经说过:状压就是个暴搜啊

乌乌,他说的对,我不写了/dk


END.

posted @   Akuto_urusu  阅读(38)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示