状压dp

朴素状压

对于数据范围小的题一定要对状压绝对敏感
这里的范围小不一定是 \(n\) 的范围小,而是任何有关信息小于 \(20\) 时要引起注意


P3052 [USACO12MAR]Cows in a Skyscraper G

相当于对每个 \(dp\) 数组开了个结构体,分别存分了几组与最后一个电梯里剩多少个
可以这样做的原因是第二维也可以贪心,没必要设计成状态


P3694 邦邦的大合唱站队

\(f[S]\) 表示前面排 \(S\) 的集合的最小交换次数,然后枚举最后一次是哪个队伍,本来就在这个区间的这个队伍的人不用移动,前缀和预处理贡献即可


P2150 [NOI2015] 寿司晚宴

这是一个典型的隐藏数据范围
很明显需要状压质因子,而质因子有一个性质是大于 \(sqrt(n)\) 的质因子最多只有一个
那么利用这个性质可以把大质数单独拎出来判断给第一个人还是给第二个人,小的进行状压
考虑把大质数相同的数一起做
开一个数组 \(g(1/2)[S1][S2]\) 表示当前质数给哪个人的方案数
注意合并时 \(f[S1][S2]=g1[S1][S2]+g2[S1][S2]-f[S1][S2]\),因为两个人都没选的情况会算重,那么需要减去(即原来的 \(f\) 值)

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=355;
const int maxm=505;
int n,mod,a[maxm],pri[maxn],vis[maxm],tot,f[maxn][maxn],g1[maxn][maxn],g2[maxn][maxn],ans,mx[maxm],S[maxm];
void pre(){
	for(int i=2;i<=n;i++){
		if(!vis[i]){
			pri[++tot]=i;
			vis[i]=i;
		}
		for(int j=1;j<=tot;j++){
			if(pri[j]*i>n||pri[j]>vis[i])break;
			vis[pri[j]*i]=pri[j];
		}
	}
	return ;
}
bool cmp(int a,int b){
	return mx[a]>mx[b];
}
int main(){
	cin>>n>>mod;
	pre();n--;
	for(int i=1;i<=n;i++){
		a[i]=i+1;
		for(int j=tot;j>=1;j--){
			if(a[i]%pri[j]==0){
				mx[a[i]]=pri[j];
				break;
			}
		}
		for(int j=1;j<=min(tot,8);j++){
			if(a[i]%pri[j]==0){
				S[a[i]]|=(1<<(j-1));
			}
		}
	}
	sort(a+1,a+n+1,cmp);
	f[0][0]=g1[0][0]=g2[0][0]=1;
//	cout<<"hhh"<<endl;
	for(int i=1;i<=n;i++){
		for(int j=255;j>=0;j--){
			for(int k=255;k>=0;k--){
				if(j&k)continue;
				if(!(j&S[a[i]]))g1[j][k|S[a[i]]]=(g1[j][k|S[a[i]]]+g1[j][k])%mod;
				if(!(k&S[a[i]]))g2[j|S[a[i]]][k]=(g2[j|S[a[i]]][k]+g2[j][k])%mod;
			}
		}
		if(mx[a[i]]!=mx[a[i+1]]){
			for(int j=0;j<=255;j++){
				for(int k=0;k<=255;k++){
					f[j][k]=(g1[j][k]+g2[j][k]-f[j][k])%mod;
					f[j][k]=(f[j][k]%mod+mod)%mod;
				}
			}
			memcpy(g1,f,sizeof g1);
			memcpy(g2,f,sizeof g2);
		}
	}
	for(int i=0;i<=255;i++){
		for(int j=0;j<=255;j++){
			ans=(ans+f[i][j])%mod;
		}
	}
	cout<<ans;
	return 0;
}

P6239 [JXOI2012]奇怪的道路

发现这道题中 \(n\) 不小,并不能作为状压的对象,那么可以转而压缩 \(k\),因为条件的特殊性,只有最后的 \(k\) 个有可能和新的点连边
那么设 \(f[i][j][S]\) 表示前 \(i\) 个点,连了 \(j\) 条边,最后 \(k\) 个状态为 \(S\) 的方案数
转移的时候枚举一个和 \(i\) 匹配的点改变状态即可

代码
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int maxn=35;
const int maxm=1005;
int n,m,k,f[maxn][maxn][maxm],all;
void modadd(int &a,int b){
	a=(a+b)%mod;
	return ;
}
int main(){
	cin>>n>>m>>k;f[2][0][0]=1;all=(1<<k+1)-1;
	for(int i=2;i<=n;i++){
		for(int p=max(1,i-k);p<=i-1;p++){
			for(int j=1;j<=m;j++){
				for(int S=0;S<=all;S++){
					modadd(f[i][j][S],f[i][j-1][S^(1<<(i-p))^1]);
				}
			}
		}
		for(int j=0;j<=m;j++){
			for(int S=0;S<=(all>>1);S++){
				modadd(f[i+1][j][S<<1],f[i][j][S]);
			}
		}
	}
	cout<<f[n][m][0];
	return 0;
}

P2157 [SDOI2009]学校食堂

一样的套路,发现 \(n\) 很大不能状压,那么转而压缩后面的 \(b_i\) 的同学
\(f[i][S][j]\) 表示 \(dp\) 到第 \(i\) 个同学,前面的都已经取完,后面的状态是 \(S\),上一次取餐的是 \(i+j\) 的最小值
转移时需要注意往下一个同学推导时必须保证这个同学已经打上饭,即最后一位为 \(1\) 的情况下,\(f[i+1][S>>1][j-1]=f[i][S][j]\)


P7519 [省选联考 2021 A/B 卷] 滚榜

时隔一年终于看懂题在干啥
很明显应该是枚举顺序的全排列,然后判断可行性直接贪心即可
由于可以贪心,那么每一个状态下的决策是固定的,具备了 \(dp\) 的条件
考虑状压,设 \(f[S][j]\) 表示已安排集合为 \(S\),选择 \(b_i\) 的和为 \(j\) 的方案数
这里用了一个“预支付”的小 \(trick\),因为需要保证 \(b\) 的单调性,那么这一个比上一个增加的 \(b\) 产生的贡献一定在后面的 \(b\) 中都有,那么直接在这一层计算即可,可以发现这样省掉了记录上一次选择的数是什么
于是每次只要计算比上一个多多少即可,还是贪心就行,注意判断编号产生的大小关系的影响,因此还需要记录一维上一次的编号是多少

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=15;
const int maxm=10005;
const int maxk=505;
int n,m,a[maxn],mx,all,f[maxm][maxn][maxk],ans,num[maxm];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int popcnt(int x){num[x]=num[x-(x&-x)]+1;return num[x];}
signed main(){
	n=read();m=read();for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=n;i++)if(a[i]>a[mx])mx=i;
	all=(1<<n)-1;
	for(int i=1;i<=n;i++){
		int w=n*(a[mx]-a[i]+(mx<i));
		f[1<<i-1][i][w]=1;
//		cout<<(1<<i-1)<<" "<<i<<" "<<w<<endl;
	}
	for(int S=1;S<all;S++){
		popcnt(S);
		for(int i=1;i<=n;i++){
			if(!((S>>i-1)&1))continue;
			for(int j=0;j<=m;j++){
				if(!f[S][i][j])continue;
				for(int k=1;k<=n;k++){
					if((S>>k-1)&1)continue;
					int w=(n-popcnt(S))*max(a[i]-a[k]+(i<k),0ll);
					if(j+w<=m)f[S|(1<<k-1)][k][j+w]+=f[S][i][j];
				}
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			ans+=f[all][i][j];
		}
	}
	cout<<ans;
	return 0;
}

P6622 [省选联考 2020 A/B 卷] 信号传递

写出距离的表达式,可以发现 \(pos_x\)\(pos_y\) 均是分开的,那么可以把贡献拆开来计算
可以发现需要预处理 \(g(i,S)\) 表示已经有 \(S\) 时对 \(i\) 的贡献,这里的空间比较卡
可以去题解区观看各种技巧,比较好的有从 \(0\)\(all\) 枚举状态那么相邻两数变化 \(bit\) 数之和为 \(O(n)\),或者可以预处理时要求 \(S\) 不包含 \(i\),空间少了 \(2\) 倍常数

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e7+5;
const int maxm=25;
int n,m,k,all,a[maxn/90],cnt[maxm][maxm],trans[maxm][maxm],to[maxm],f[maxn],re[maxn],siz[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
void domin(int &a,int b){if(b<a)a=b;}
int main(){
	n=read(),m=read(),k=read();all=(1<<m)-1;
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<n;i++)cnt[a[i]][a[i+1]]++;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=m;j++){
			if(i==j)continue;
			trans[i][j]=cnt[i][j]*(k+1)-cnt[j][i]*(k-1);
			to[i]+=cnt[j][i]*k-cnt[i][j];
		}
	}
	for(int i=1;i<=all;i++)siz[i]=siz[i>>1]+(i&1);
	for(int i=0;i<m;i++)re[1<<i]=i+1;
	memset(f,0x3f,sizeof f);f[0]=0;
	for(int S=0;S<all;S++){
//		for(int i=0;i<m;i++)if(!(S>>i&1))domin(f[S|1<<i],f[S]+to[i+1]*(siz[S]+1));
		for(int T=S^all;T;T-=T&-T)domin(f[S|(T&-T)],f[S]+to[re[T&-T]]*(siz[S]+1));	
		for(int T=S^(S+1);T;T-=T&-T){
			int x=T&-T;
			if(x&S)for(int i=1;i<=m;i++)to[i]-=trans[i][re[x]];	
			else for(int i=1;i<=m;i++)to[i]+=trans[i][re[x]];
		}
//		for(int i=1;i<=m;i++)cout<<to[i]<<" ";puts("");
	}
	cout<<f[all];
	return 0;
}


枚举子集

这是一个重要的状压 \(trick\),枚举一个集合子集(补集)的子集的时间复杂度是 \(3^n\)
代码这样实现:

for(int T=S;T;T=(T-1)&S)

注意在计数型状压 \(dp\) 中,子集式的转移经常会将答案算重,比如算成达到某一状态的步骤方案数等等
那么可以通过钦定状压转移方式来解决,比如钦定新的状态 \(T\) 一定包含 \(S\)\(lowbit\),有时候可以通过除以阶乘的方式规避但并不是很通用


斯坦纳树

斯坦纳树是用于解决在边/点带权无向图上求出选点使得关键点连通的最小代价问题,其中关键点特别少

以边权为例
首先可以发现最终一定形成了一棵树,设 \(f[S][i]\) 表示集合 \(S\) 被连通,当前树根为为 \(i\) 的代价
那么转移首先有拼接操作,那么枚举一个点的子树,进行子集合并即可,\(f[i][S]+f[i][T]->f[i][S+T]\)
第二种转移为加入一个新的点,\(f[S][i]+w(i,j)->f[S][j]\)
这个转移成环,那么去泡最短路

注意这里的 \(dp\) 是首先以 \(S\) 为下标的,因此每做完一个 \(S\) 就已经算出这个集合的答案了

这里 是模板

如果是点权版本,那么转移式变成了 \(f[i][S]+f[i][T]-a[i]->f[i][S+T]\),因为此时根节点多算了一次(注意是多算,因为一个点在被合并的时候一定已经经过二转移了,其贡献此时已经加入),比如 这个

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
const int maxm=1105;
const int inf=0x3f3f3f3f;
int n,m,k,num[maxn],hd[maxn],cnt,f[maxn][maxm],vis[maxn],x,y,w,all;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct Edge{
	int nxt,to,val;
}edge[maxm];
void add(int u,int v,int w){
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	edge[cnt].val=w;
	hd[u]=cnt;
	return ;
}
struct Node{
	int dis,id;
	Node(){}
	Node(int x,int y):dis(x),id(y){}
	bool operator < (const Node  & x) const {
		return dis>x.dis;
	}
};
priority_queue<Node>q;
void dij(int S){
	memset(vis,0,sizeof vis);
	while(!q.empty()){
		int u=q.top().id;q.pop();
		if(vis[u])continue;vis[u]=true;
		for(int i=hd[u];i;i=edge[i].nxt){
			int v=edge[i].to;
			if(f[v][S]>f[u][S]+edge[i].val){
				f[v][S]=f[u][S]+edge[i].val;
				q.push(Node(f[v][S],v));
			}
		}
	}
	return ;
}
int main(){
	n=read(),m=read(),k=read();memset(f,0x3f,sizeof f);
	for(int i=1;i<=m;i++)x=read(),y=read(),w=read(),add(x,y,w),add(y,x,w);
	for(int i=1;i<=k;i++){
		num[i]=read();f[num[i]][1<<i-1]=0;
	}
	all=(1<<k)-1;
	for(int S=1;S<=all;S++){
		for(int i=1;i<=n;i++){
			for(int T=S&(S-1);T;T=S&(T-1)){
				f[i][S]=min(f[i][S],f[i][T]+f[i][S-T]);
			}
			if(f[i][S]!=inf)q.push(Node(f[i][S],i));
		}
		dij(S);
	}
	cout<<f[num[1]][all];
	return 0;
}

P3264 [JLOI2015]管道连接

由于斯坦纳树求解的时候是求出所有颜色集合连通的代价的,那么以这个作为集合权值再跑一次子集转移

posted @ 2022-08-17 10:30  y_cx  阅读(83)  评论(4编辑  收藏  举报