首页 > 其他分享 >状压dp

状压dp

时间:2022-08-17 10:33:47浏览次数:95  
标签:ch const int 状压 maxn maxm 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]管道连接

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

标签:ch,const,int,状压,maxn,maxm,dp
From: https://www.cnblogs.com/yang-cx/p/15531209.html

相关文章

  • 达梦dexpdp,dimpdp导出导入数据
    1.创建directorySQL>createdirectorydirectas'/dm8/direct';executedsuccessfullyusedtime:48.272(ms).Executeidis503.2.创建测试用户SQL>createuse......
  • 「AGC012F」Prefix Median 题解 (DP)
    题目简介给定一个长度为\(2n-1\)的序列\(a\),你可以随意排列\(a\)中的元素,请求出有多少种不同的序列\(b\),满足\(b\)的长度为\(n\)。\(b_i=\{a_1\ldotsa_{2......
  • 期望dp
    期望的线性性质:E(ax+by)=aE(X)+bE(Y)1-n总长度的期望到达某个结果的期望值=这个结果*从起始状态到这个状态的概率f[i]=∑1/k*(w[i]+f(S[i])f[i]表示从i走到n的期......
  • Codeforces1699E Three Days Grace【数学】【DP】
    分析:一开始觉得是二分答案,发现行不通之后改为枚举最小值。现在我将这若干个数分解,假设分解完之后得到的最小值为$i$,那么我就是要在最小值为$i$的基础上尽量最小化分解的......
  • 8、ThreadPoolTaskExecutor线程并发
    一、线程池的优点:1、降低资源消耗。通过重复利用自己创建的线程降低线程创建和销毁造成的消耗。2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行......
  • cf C. Vertex Deletion 树形DP 删除某写节点 且保证其它节点都有至少一个相连节点的总
     https://codeforces.com/gym/103145/problem/C timelimitpertest1.5secondsmemorylimitpertest256megabytesinputstandardinputoutputstandard......
  • 重修 斜率优化 Dp
    斜率单调暴力移指针斜率不单调二分找答案\(x\)坐标单调开单调队列\(x\)坐标不单调开平衡树/cdq分治P4072[SDOI2016]征途我们要求方差最小,而总和不变,等价于要每......
  • 洛谷P2622 关灯问题II引发的关于DP实现形式及后效性的思考
      动态规划要求已经求解的子问题不受后续阶段的影响,即无后效性。而在这种递推的实现方式中,后面枚举的状态可能更新前面已经枚举过的状态。也就是说,这种递推的实现方式是......
  • ubuntu dpkg问题解决
    问题今天玩ubuntu发现以下报错:dpkgwasinterrupted,youmustmanuallyrunsudodpkg–configure-atocorrecttheproblem 解决sudorm/var/lib/apt/lists/l......
  • TCP和UDP的应用场景
    传输层的两个协议,TCP(TransmissionControlProtocol,传输控制协议)和UDP(UserDatagramProtocol,用户数据报协议),有各自的应用场景。TCP应用场景TCP为应用层协议提供可靠传输......