YbtOJ 「动态规划」第5章 状压DP

犹豫了许久还是决定试试始终学不会的状压 dp。(上一次学这东西可能还是两年前的网课,显然当时在摸鱼一句都没听/kk
果然还是太菜。

例题1.种植方案

\(f_{i,j}\) 表示第 \(i\) 行状态为 \(j\) 时的方案数。转移时判断一下满不满足情况。
\(f_{i,j}=\Sigma f_{i-1,k}\)

code
#include<bits/stdc++.h>
using namespace std;
const int mod=1e8;
int m,n,sta[4100],cnt;
int f[15][4100],a[15];
int main()
{
	scanf("%d%d",&m,&n);
	for(int i=1;i<=m;i++)
	{
		for(int j=1,x;j<=n;j++)
		{
			scanf("%d",&x);
			a[i]=(a[i]<<1)|(!x);
		}
	}
	for(int i=0;i<(1<<n);i++)
	{
		if(i&(i<<1)) continue;
		sta[++cnt]=i;
	}
	f[0][1]=1;
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			if(a[i]&sta[j]) continue;
			for(int k=1;k<=cnt;k++)
			{
				if((a[i-1]&sta[k])||(sta[j]&sta[k])) continue;
				f[i][j]=(f[i][j]+f[i-1][k])%mod;
			}
		}
	}
	int ans=0;
	for(int i=1;i<=cnt;i++) ans=(ans+f[m][i])%mod;
	cout<<ans<<endl;
	return 0;
}

例题2.最短路径

\(f_i,j\) 表示当前在第 \(i\) 个点且走过的状态为 \(j\) 时的最短路径。
枚举上一步的位置 \(k\) 进行转移。

code
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9;
int n,mp[25][25];
int f[25][1100000];
int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			scanf("%d",&mp[i][j]);
	for(int i=0;i<n;i++)
		for(int j=0;j<(1<<n);j++) f[i][j]=inf;
	f[0][1]=0;
	for(int j=2;j<(1<<n);j++)
	{
		for(int i=1;i<n;i++)
		{
			if(!(j&(1<<i))) continue;
			for(int k=0;k<n;k++)
			{
				if(i==k) continue;
				if(!(j&(1<<k))) continue;
				int now=j&(~(1<<i));
				f[i][j]=min(f[i][j],f[k][now]+mp[k][i]);
			}
		}
	}
	cout<<f[n-1][(1<<n)-1]<<endl; 
	return 0;
}

例题3.涂抹果酱

三进制的状压。先 dfs 出每种状态,开一个数组存起来。
预处理每两种状态能否出现在相邻的两行。
\(K\) 行的上面和下面分别 dp。根据乘法原理,最后的答案是上下两部分乘起来。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e6;
int n,m,cnt,K,sta[305][10];
bool flag[305][305];
int f[10005][305],g[10005][305];
int a[10],kk[10],num;
void dfs(int now)
{
	if(now==m+1)
	{
		cnt++;
		for(int i=1;i<=m;i++) sta[cnt][i]=a[i];
		return;
	}
	for(int i=1;i<=3;i++)
	{
		if(i==a[now-1]) continue;
		a[now]=i;dfs(now+1);
	}
}
void init()
{
	dfs(1);
	for(int i=1;i<=cnt;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			if(i==j) continue;
			bool qwq=0;
			for(int k=1;k<=m;k++)
				if(sta[i][k]==sta[j][k]) qwq=1;
			flag[i][j]=(!qwq);
			//cout<<i<<" "<<j<<" "<<flag[i][j]<<endl;
		}
	}
}
signed main()
{
	scanf("%lld%lld",&n,&m);init();
	scanf("%lld",&K);
	for(int i=1;i<=m;i++) scanf("%lld",&kk[i]);
	for(int i=1;i<=cnt;i++)
	{
		int qwq=0;
		for(int j=1;j<=m;j++)
			if(sta[i][j]!=kk[j]) qwq=1;
		if(!qwq) num=i;
	}
	f[K][num]=1;
	for(int i=K-1;i;i--)
		for(int j=1;j<=cnt;j++)
			for(int k=1;k<=cnt;k++)
			{
				if(!flag[j][k]) continue;
				f[i][j]=(f[i][j]+f[i+1][k])%mod;
			}
	int up=0;
	if(K==1) up=1;
	else for(int i=1;i<=cnt;i++) up=(up+f[1][i])%mod;
	for(int i=K+1;i<=n;i++)
		for(int j=1;j<=cnt;j++)
			for(int k=1;k<=cnt;k++)
			{
				if(!flag[j][k]) continue;
				f[i][j]=(f[i][j]+f[i-1][k])%mod;
			}
	int down=0;
	if(K==n) down=1;
	else for(int i=1;i<=cnt;i++) down=(down+f[n][i])%mod;
	cout<<up*down%mod<<endl;
	return 0;
}

例题4.炮兵阵地

感觉这题是种植方案+互不侵犯套在一起(?
\(f_{i,j,k}\) 表示第 \(i\) 行状态为 \(j\),上一行状态为 \(k\) 时最多能放置的炮兵数。平原和山地的问题参照例题 1。
因为当前行的状态与前两行都有关,所以转移同时分别枚举前两行的状态。

code
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt;
int mp[105],tot[95];
int sta[105],f[105][95][95];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<(1<<m);i++) 
	{
		if((i&(i<<1))||(i&(i<<2))) continue;
		sta[++cnt]=i;
		for(int j=0;j<m;j++) if((i>>j)&1) tot[cnt]++;
	} 
	for(int i=1;i<=n;i++)
		for(int j=0;j<m;j++)
		{
			char qwq;cin>>qwq;
			mp[i]=(mp[i]<<1)|(qwq=='H');
		}
	for(int i=1;i<=cnt;i++) f[1][i][1]=tot[i];
	for(int i=2;i<=n;i++)
	{
		for(int j=1;j<=cnt;j++)
		{
			if(sta[j]&mp[i]) continue;
			for(int k=1;k<=cnt;k++)
			{
				if((sta[k]&mp[i-1])||(sta[k]&sta[j])) continue;
				for(int l=1;l<=cnt;l++)
				{
					if((sta[l]&mp[i-2])||(sta[l]&sta[k])||(sta[l]&sta[j])) continue;
					f[i][j][k]=max(f[i][j][k],f[i-1][k][l]+tot[j]);
				}
			}
		}
	}
	int ans=0;
	for(int i=1;i<=cnt;i++) 
		for(int j=1;j<=cnt;j++)
			ans=max(ans,f[n][i][j]);
	cout<<ans<<endl;
	return 0;
}

1.最优组队

\(f_i\) 表示当前分组的状态为 \(i\) 时最大和谐度。每一位 \(0/1\) 表示这个位置上的人是否已经被分组。
枚举 \(i\) 的子集进行转移。
枚举子集方法:

for(int j=i;j;j=(j-1)&i){
    int k=j^i;
}

\(j\)\(i\) 的子集,\(k\)\(j\)\(i\) 内的补集。

code
#include<bits/stdc++.h>
using namespace std;
const int N=66000;
int n,s[N];
int f[N];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<(1<<n);i++) scanf("%d",&s[i]);
	for(int i=1;i<(1<<n);i++)
	{
		f[i]=s[i];
		for(int j=i;j;j=(j-1)&i)
		{
			int k=j^i;
			//cout<<i<<" "<<j<<" "<<k<<endl;
			f[i]=max(f[i],f[j]+s[k]);
		}
	}
	cout<<f[(1<<n)-1]<<endl;
	return 0;
}

2.最短路径

只需要知道标记点到其他点的距离。对起点、终点、每个标记点跑一遍 dijkstra。
转移可以仿照例题 2 的思路。

code
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e5+5;
const int inf=1e18;
int n,m,K,s,t;
int a[15];
int head[N],cnt;
struct node{
	int nxt,to,w;
}e[N];
void add(int u,int v,int w){
	e[++cnt]={head[u],v,w};head[u]=cnt;
}
int dis[15][N],f[15][4100];
void dij(int s,int id)
{
	priority_queue<pii,vector<pii>,greater<pii> > q;
	for(int i=0;i<n;i++) dis[id][i]=inf;
	dis[id][s]=0;q.push(pii(0,s));
	while(!q.empty())
	{
		int u=q.top().se;q.pop();
		for(int i=head[u];i;i=e[i].nxt)
		{
			int v=e[i].to;
			if(dis[id][u]+e[i].w<dis[id][v]) 
			{
				dis[id][v]=dis[id][u]+e[i].w;
				q.push(pii(dis[id][v],v));
			}
		}
	}
}
signed main()
{
	scanf("%lld%lld%lld%lld%lld",&n,&m,&K,&s,&t);
	s--;t--;
	for(int i=1,u,v,w;i<=m;i++)
	{
	 	scanf("%lld%lld%lld",&u,&v,&w);u--;v--;
	 	add(u,v,w);
	}
	dij(s,0),dij(t,K+1);a[0]=s,a[K+1]=t;
	for(int i=1;i<=K;i++) scanf("%lld",&a[i]),a[i]--,dij(a[i],i); 
	for(int i=0;i<=K+1;i++) for(int j=0;j<(1<<(K+2));j++) f[i][j]=inf; 
	f[0][1]=0;K+=2;
	for(int j=2;j<(1<<K);j++)
	{
		for(int i=0;i<K;i++)
		{
			if(((1<<i)&j)==0) continue;
			for(int k=0;k<K;k++)
			{
				if(((1<<k)&j)==0||i==k) continue;
				int l=j&(~(1<<i));
				//cout<<i<<" "<<j<<" "<<k<<" "<<l<<endl;
				f[i][j]=min(f[i][j],f[k][l]+dis[k][a[i]]);
				//cout<<f[i][j]<<" "<<f[k][l]<<" "<<dis[k][a[i]]<<endl;
			}
		}
	}
	if(f[K-1][(1<<K)-1]==inf) cout<<-1<<endl;
	else cout<<f[K-1][(1<<K)-1]<<endl;
	return 0;
}

3.小绿小蓝

状态设为点集,则要求边权和最大。
最后枚举状态统计答案。

code
#include<bits/stdc++.h>
using namespace std;
const int N=20,M=1.4e5,inf=2e9;
int n,m,mp[N][N];
int a[N],s,t;
int f[N][M],sa[M]; 
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;i++) scanf("%d",&a[i]);
	for(int i=1,u,v,w;i<=m;i++)
	{
		scanf("%d%d%d",&u,&v,&w);
		mp[u-1][v-1]=max(mp[u-1][v-1],w);
	}
	scanf("%d%d",&s,&t);s--;t--;
	for(int i=0;i<(1<<n);i++)
		for(int j=0;j<n;j++) sa[i]+=((i>>j)&1)*a[j];
	for(int i=0;i<n;i++) for(int j=0;j<(1<<n);j++) f[i][j]=-inf;
	f[s][1<<s]=0;
	for(int j=0;j<(1<<n);j++)
	{
		for(int i=0;i<n;i++)
		{
			if((j&(1<<i))==0) continue;
			for(int k=0;k<n;k++)
			{
				if((j&(1<<k))==0||k==i) continue;
				if(!mp[k][i]) continue;
				int l=j&(~(1<<i));
				f[i][j]=max(f[i][j],f[k][l]+mp[k][i]);
			}
		}
	}
	double minn=inf;
	for(int i=0;i<(1<<n);i++)
	{
		if(f[t][i]<0) continue;
		minn=min(sa[i]*1.0/f[t][i],minn);
	}
	printf("%.3lf\n",minn);
	return 0;
}

4.擦除序列

预处理每个状态是不是回文串。

code
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9,M=70000,N=20;
int n,f[M],ch[M];
char s[N];
int main()
{
	scanf("%s",s);n=strlen(s);
	for(int i=0;i<(1<<n);i++)
	{
		int now[N],cnt=0;
		for(int j=0;j<n;j++) 
		{
			if(i&(1<<j)) now[++cnt]=j;
		}
		bool flag=0;
		for(int j=1;j<=cnt;j++)
		{
			int k=cnt-j+1;
			if(s[now[j]]!=s[now[k]]) flag=1;
		}
		ch[i]=(!flag);
	}
	for(int i=0;i<(1<<n);i++) f[i]=inf;
	f[0]=0;
	for(int i=1;i<(1<<n);i++)
	{
		if(ch[i]) f[i]=1;
		for(int j=i;j;j=(j-1)&i)
		{
			int k=j^i;
			if(!ch[j]) continue;
			f[i]=min(f[i],f[k]+1);
		}
	}
	cout<<f[(1<<n)-1]<<endl;
	return 0;
}

5.图的计数

真·摆了一天。
\(f_{i,j}\) 表示第 \(i\) 行的状态(每个点的奇偶性)为 \(j\) 时的最小反转数。
反转边时使用异或转移。

code
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9,mod=998244353;
const int N=1e4+5,M=(1<<10)+5;
int n,m,a[N][M],b[N][M];
int f[N][M];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=0,x;i<m;i++)
	{
		scanf("%d",&x);
		a[0][0]+=x*(1<<i);
	}
	f[2][a[0][0]]=1;
	for(int i=2;i<=n-2;i++)
	{
		for(int k=1;k<=m;k++)
		{
			for(int p=1,x;p<=m;p++)
			{
				scanf("%d",&x);
				a[i][k]+=x*(1<<(p-1));
				b[i][p]+=x*(1<<(k-1));
			}
		}
	}
	for(int i=3;i<=n-1;i++)
	{
		for(int j=0;j<(1<<m);j++)
		{
			if(f[i-1][j])
			{
				int s1=0,s2=0;
				for(int k=1;k<=m;k++)
				{
					if(j&1<<(k-1)) s1^=a[i-1][k],s2^=b[i-1][k];
				}
				//(f[i][s1]+=f[i-1][j])%=mod;
				f[i][s1]=(f[i][s1]+f[i-1][j])%mod;
				f[i][s2]=(f[i][s2]+f[i-1][j])%mod;
			}
		}
	}
	for(int i=1,x;i<=m;i++)
	{
		scanf("%d",&x);
		a[n-1][0]+=x*(1<<(i-1));
	}
	int ans=0;
	for(int j=0;j<(1<<m);j++)
	{
		int tot=0;
		for(int i=1;i<=m;i++)
		{
			if((a[n-1][0]&(1<<(i-1)))&&(j&(1<<(i-1)))) tot^=1;
		}
		if(!tot) ans=(ans+f[n-1][j])%mod;
	}
	cout<<ans<<endl;
	return 0; 
}

posted @ 2022-08-18 14:26  樱雪喵  阅读(99)  评论(0编辑  收藏  举报