状压dp专题

经典的状压dp

先考虑横着放 如果横着放的方案确定了 那么竖着放的也就唯一确定了

所以总方案数=横着放的方案数

但是可能我们横着放完了后 留下的空间竖着放怎么都不能放满(也就是竖着连续对的0为奇数)不合法

这个我们可以预处理

定义方程:设dp[i,j]表示前i列已经放完横木块且第i列的状态为j的总方案数

例如j=010110 则表示第二,四,五行有木块捅到后面一列去(也就是横着放的木块的头子在第i列的第2,4,5行)

转移方程:dp[i,j]+=dp[i-1,k] 其中j和k状态必须合法

合法条件:1, j&k=0 因为防止木块重合
2, 第i列合法(第i列的木块包括第i-1列捅过来的和第i列捅出去的)
初始状态dp[0,0]=1
终止状态dp[m,0](第m列不能再往后捅了)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=12;
int n,m;
ll dp[maxn][1<<(maxn-1)];
int pd[1<<(maxn-1)];
int main(){
	cin>>n>>m;
	while(n!=0&&m!=0){
		for(int i=0;i<1<<n;i++){
			int cnt=0;
			pd[i]=true;
			for(int j=0;j<n;j++){
				if((i>>j)&1){
					if(cnt&1){
						pd[i]=false;
					}else cnt=0;
				}else cnt++;
			}
			if(cnt&1)pd[i]=false;
		}
		memset(dp,0,sizeof(dp));
		dp[0][0]=1;
		for(int i=1;i<=m;i++){
			for(int j=0;j<(1<<n);j++){
				for(int k=0;k<(1<<n);k++){
					if(!(j&k)&&pd[j|k])
					dp[i][j]+=dp[i-1][k];
				}
			}
		}
		cout<<dp[m][0]<<endl;
		cin>>n>>m;
	}
     return 0;
}

这个也是很经典的一道状压dp 比上面那道要简单

设dp[i,j]表示已经走过的城市状态为i,且最后一个城市为j

初始状态 dp[1,0]=0

终止状态 dp[(1<<n)-1,n-1]

转移方程 dp[i,j]=min(dp[i,j],dp[i-(1<<j),k]+w[k][j]) 其中j和k为i状态里面互不相同的城市

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
#define inf 1e9
const int N=20;
const int M=1<<19;
int dp[M][N],w[N][N];
int n;
int main(){
	cin>>n;
	for(int i=0;i<n;i++)
	for(int j=0;j<n;j++)
	cin>>w[i][j];
	memset(dp,0x3f,sizeof(dp));
	dp[1][0]=0;
	for(int i=0;i<1<<n;i++)
		for(int j=0;j<n;j++)
		if((i>>j)&1)
		for(int k=0;k<n;k++)
		if(((i>>k)&1)&&k!=j)
		dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+w[j][k]);
		cout<<dp[(1<<n)-1][n-1];
     return 0;
}
/*
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
*/

感觉这个题目放在这个专题多不好 因为正解是肯定不能用状压dp的

可以用状压 但是一定不能用dp

因为这个题目无法保证无后效性 简而言之

如果我们从大到小开始枚举状态 此时状态为 i 在按下一个按钮后状态为 j

此时 j可能大于i可能小于i 这样我们从大到小开始枚举状态的意义何在?

出这个题目的人想法很好 但是对dp理解还不够深刻

放出状压dp的code(错的但是能过)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=10;
const int maxm=105;
int n,m;
int a[maxm][maxn];
int dp[1<<(maxn+1)];
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	for(int j=0;j<n;j++)
	cin>>a[i][j];
	memset(dp,0x7f,sizeof(dp));
	dp[(1<<n)-1]=0;
	for(int i=(1<<n)-1;i>=0;i--){
		for(int num=1;num<=m;num++){
		int j=i;
			for(int k=0;k<n;k++){
				if(a[num][k]==1){
			if((i>>k)&1)j-=(1<<k);
		}else if(a[num][k]==-1){
			if(!((i>>k)&1))j+=(1<<k);
		}
			}
		dp[j]=min(dp[j],dp[i]+1);
		}
	}
	if(dp[0]!=2139062143)
	cout<<dp[0]<<endl;
	else cout<<"-1"<<endl;
     return 0;
}

正解就只能是bfs+状压

正确的code:

点击查看代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;

int n, m;
int a[110][15];
bool vis[2000];
int step[2000];
queue<int> q;

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; ++i) {
		for (int j = 1; j <= n; ++j) {
			scanf("%d", &a[i][j]);
		}
	}
	q.push((1 << n) - 1);
	vis[(1 << n) - 1] = true;
	while (q.size()) {
		int tx = q.front(); q.pop();
		if (!tx) { printf("%d", step[tx]); return 0; }
		for (int i = 1; i <= m; ++i) {
			int ttx = tx;
			for (int j = 1; j <= n; ++j) {
				if (a[i][j] == 1 && (tx & (1 << j - 1))) ttx &= ~(1 << (j - 1));//此处判断 ttx & (1 << j - 1) 亦可,因为当前位置 j 的值并未被修改 
				if (a[i][j] == -1 && !(tx & (1 << j - 1))) ttx |= 1 << (j - 1);
			}
			if (!vis[ttx]) q.push(ttx), vis[ttx] = true, step[ttx] = step[tx] + 1;
		}
	}
	printf("-1");
	return 0;
}

首先一看数据 状压dp跑不掉了

代码是w[i,j]是把i 全部放在 j 后面需要的步数 两种是一样的

点击查看代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
#include <climits>
#include <cstdlib>
using namespace std;
const int MAXN = 4e5 + 3;
int n , a[MAXN] ;
long long w[23][23];
long long dp[1<<20+2];
long long cnt[MAXN];
int main(){
    scanf( "%d" , &n );
    for( int i = 1 ; i <= n ; i ++ ){
        scanf( "%d" , &a[i] );cnt[a[i]-1] ++;
        for( int j = 0; j < 20 ; j ++ )
            w[j][a[i]-1] += cnt[j];
    }
    dp[0] = 0;
    for( int i = 1 ; i < ( 1 << 20 ) ; i ++ ){
        dp[i] = LLONG_MAX;
        for( int j = 0 ; j < 20 ; j ++ ){
            if( i & ( 1 << j ) ){
                int k = i ^ ( 1 << j );
                long long sum =0 ;
                for( int l = 0 ; l < 20 ; l ++ ){
                    if( l != j && ( k & ( 1 << l ) ) ){
                        sum += w[j][l];
                    }
                }
                dp[i] = min( dp[i] , dp[k] + sum );
            }
        }
    }
    printf( "%lld" , dp[(1<<20)-1] );
}

本来找网络流24题的 但是发现这个题直接一个状压 +最短路就好了

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=22;
int n,m;
int vis[(1<<maxn)],a[maxn*10],b[maxn*10],c[maxn*10],d[maxn*10];
int dis[(1<<maxn)],val[maxn];
string s; 
void spfa(){
	queue<int>Q;
	memset(dis,0x7f,sizeof(dis));
	dis[(1<<n)-1]=0;
	Q.push((1<<n)-1);
	while(!Q.empty()){
		int u=Q.front();
		Q.pop();vis[u]=0;
		for(int i=1;i<=m;i++)
			if((u&a[i])==a[i]&&(u&b[i])==0){
			int to=((u|c[i])|d[i])^c[i];
			if(dis[to]>dis[u]+val[i]){
				dis[to]=dis[u]+val[i];
				if(!vis[to]){
					vis[to]=1;
					Q.push(to);
				}
			}	
			}
	}
	if(dis[0]==dis[(1<<maxn)-1])
	cout<<0<<endl;
	else 
	cout<<dis[0]<<endl;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>val[i]>>s;
		for(int j=0;j<n;j++)
		if(s[j]=='+')a[i]|=(1<<j);
		else if(s[j]=='-')b[i]|=(1<<j);
		cin>>s;
		for(int j=0;j<n;j++)
		if(s[j]=='-')c[i]|=(1<<j);
		else if(s[j]=='+')d[i]|=(1<<j);
	}
	spfa();
     return 0;
}

牛可乐的翻转游戏

题目描述:
链接:https://ac.nowcoder.com/acm/problem/235250
牛可乐发明了一种新型的翻转游戏!

在一个有 n 行 m 列的棋盘上,每个格子摆放有一枚棋子,每一枚棋子的颜色要么是黑色,要么是白色。每次操作牛可乐可以选择一枚棋子,将它的颜色翻转(黑变白,白变黑),同时将这枚棋子上下左右相邻的四枚棋子的颜色翻转(如果对应位置有棋子的话)。

牛可乐想请你帮他判断一下,能否通过多次操作将所有棋子都变成黑色或者白色?如果可以,最小操作次数又是多少呢?

和这个题思路比较类似:https://www.cnblogs.com/wzxbeliever/p/16465819.html

只要将第一行的状态先确定了 接下来每行的操作一定是确定的 所以只要枚举第一行的情况 然后顺着推到最后一行 如果最后一行和目标能够一致 那么就能行

code:

#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
int n,m,ans=1000;
int a[105][11],now[105][11];
int dx[5]={0,-1,1,0,0};
int dy[5]={0,0,0,-1,1};
int calc(int state,int pd);
void turn(int in,int im);
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=0;j<m;j++)
	scanf("%1d",&a[i][j]);
	for(int i=0;i<(1<<m);i++)
		ans=min(ans,min(calc(i,0),calc(i,1)));
	if(ans!=1000)cout<<ans<<endl;
	else cout<<"Impossible"<<endl;
     return 0;
}
void turn(int in,int im){
	for(int i=0;i<=4;i++){
		int a=in+dx[i],b=im+dy[i];
		if(a>=1&&a<=n&&b>=0&&b<m)
		now[a][b]^=1;
	}
}
int calc(int state,int pd){
	int res=0;
	for(int i=1;i<=n;i++)
	for(int j=0;j<m;j++)
	now[i][j]=a[i][j];
	for(int i=0;i<m;i++)
	if(state&(1<<i))
	turn(1,i),res++;
	for(int i=2;i<=n;i++)
	for(int j=0;j<m;j++)
	if(now[i-1][j]!=pd)
	turn(i,j),res++;
	for(int i=0;i<m;i++)
	if(now[n][i]!=pd)
	return 1000;
	return res;
}

https://www.jisuanke.com/problem/A1951

分析:

一道非常裸的状压dp 但是我写挂了两次 还是不太熟练 需要多加练习

第一次写挂 :直接利用bfs转移 超空间限制了 有很多状态重复

第二次写挂:因为选择的集合是逐渐递增的 无后效性 换成写dp 枚举了时间和状态 超时间了

第三次 发现根本不需要枚举时间 状态有多少个1 就用了多少时间

#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=(1<<20)+2;
ll a[23],b[23],ans=-4485090715960753727;
int n;
vector<int>Q[21];
ll dp[maxn];
void solve();
int main(){
	int T;T=1;
	while(T--)solve();
     return 0;
}
void solve(){
     cin>>n;
     for(int i=1,num;i<=n;i++){
     	cin>>a[i]>>b[i]>>num;
     	for(int j=1,x;j<=num;j++)
		 cin>>x,Q[i].push_back(x);
	 }
	 memset(dp,-0x3f,sizeof(dp));
	 dp[0]=0;
	 for(int i=0;i<(1<<n);i++){
	 	ll T=__builtin_popcount(i);
	 		for(int u=1;u<=n;u++)
	 		if(!(i&(1<<(u-1)))){
	 			bool pd=1;
	 			for(int j=0;j<Q[u].size();j++)
	 			if(!(i&(1<<(Q[u][j]-1)))){
	 				pd=0;break;
				 }
				 if(!pd)continue;
				 dp[i|(1<<(u-1))]=max(dp[i|(1<<(u-1))],dp[i]+a[u]*(T+1)+b[u]);
			 }
	 }
	for(int i=0;i<(1<<n);i++)
	ans=max(ans,dp[i]);
	cout<<ans;
}

https://ac.nowcoder.com/acm/problem/24158

分析: 练习多了之后 打起来就非常顺手了

又是一道非常明显的状压dp

因为时间太大 不能存入dp数组中 那就把时间放到dp结果当中

dp[S] 表示选点状态为 S 最大能看的时间

转移的时候 我们找到没在S中的点 找到开始时间小于等于dp[S] 最大的开始时间

#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=(1<<21)+2;
int n,ans=100;
ll x,L;
ll dur[25],dp[maxn];
vector<ll>Q[25];
vector<ll>:: iterator id;
void solve();
int main(){
	int T;T=1;
	while(T--)solve();
     return 0;
}
void solve(){
    cin>>n>>L;
    for(int i=1,num;i<=n;i++){
    	cin>>dur[i]>>num;
    	for(int j=1;j<=num;j++)
		cin>>x,Q[i].push_back(x); 
		sort(Q[i].begin(),Q[i].end()); 
	}

	for(int i=0;i<(1<<n);i++){
		for(int j=1;j<=n;j++)
		if(!(i&(1<<(j-1)))){
			id=lower_bound(Q[j].begin(),Q[j].end(),dp[i]);
			if(id==Q[j].end())continue;
			if(*id==dp[i]) 
			dp[i|(1<<(j-1))]=max(dp[i|(1<<(j-1))],dp[i]+dur[j]);
			else if(id!=Q[j].begin()){
				id--;
				dp[i|(1<<(j-1))]=max(dp[i|(1<<(j-1))],*id+dur[j]);
			}
		}
	}
	for(int i=0;i<(1<<n);i++)
	if(dp[i]>=L)ans=min(ans,__builtin_popcount(i));
	if(ans!=100)
	cout<<ans;
	else cout<<"-1";
}

https://codeforces.com/gym/102219/problem/F?f0a28=2

题意:

给两个 1 - n的序列,要求序列中的数两两配对,使得配对的两个数绝对值之差小于 e ,并且还有 k 对限制,即 u 不能和 v 配对。

分析:

因为e很小 所以想到状压 设第一个序列为A 第二个序列为B 两个序列都是 1 2 3 4 . . . n

我们依次考虑A序列中每个数i 与之匹配的B序列可能的位置范围在[i-e,i+e] B中这2e+1个数的状态我们可能枚举出来

然后按照顺序遍历一遍即可

代码中的X>>1 是整个状态区间是要向右移动的

#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const int mod=1e9+7;
typedef long long ll;
ll f[maxn][maxn],g[maxn][maxn],ans;
int n,e,k;
int main () {
    scanf("%d%d%d",&n,&e,&k);
    for (int i=0;i<k;i++) {
        int x,y;
        scanf("%d%d",&x,&y);
        g[x][y]=1;
    }
    f[0][0]=1;
    for (int i=1;i<=n;i++) 
        for (int x=0;x<(1<<2*e+1);x++)
            for (int j=-e;j<=e;j++) {
                int k=i+j;
                if (k<1||k>n||g[i][k]) continue;
                if ((x>>1)&(1<<(j+e))) continue;
                f[i][(x>>1)|(1<<(j+e))]=f[i][(x>>1)|(1<<(j+e))]+f[i-1][x];
                f[i][(x>>1)|(1<<(j+e))]%=mod;
            }
    for (int i=0;i<(1<<2*e+1);i++) ans+=f[n][i],ans%=mod;
    printf("%lld\n",ans);
}

https://codeforces.com/contest/906/problem/C

题意:n个人,m条信息,每条信息为(x, y)表示x和y认识。每次操作可以选取一个人,让他的所有朋友相互认识。求使得所有人相互认识的最少操作次数以及对应的方案。(n<=22)

分析:

很明显的状压dp dp[i]表示状态i中的人已经互相认识的最小操作数 因为每次操作都会使得状态中1的个数递增的 所以按照顺序遍历是没有后效性的

因为dp表示状态i中的人已经互相认识 所以对i中任意一个人j的操作下一个状态就是i|sta[j] 其中sta[j]预处理出来

巧妙利用设计的状态已知的条件

#include<bits/stdc++.h>
using namespace std;
#define lowbit(x) x&(-x)
#define ll long long
const int maxn=23;
int n,m;
int sta[maxn],dp[1<<maxn],pre[1<<maxn],id[1<<maxn];
void solve();
int main(){
	int T;T=1;
	while(T--)solve();
     return 0;
}
void solve(){
	scanf("%d%d",&n,&m);
	memset(id,-1,sizeof(id));
	memset(dp,0x7f,sizeof(dp));
	for(int x,y,i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		x--;y--;
		sta[x]|=(1<<y);
		sta[y]|=(1<<x);
		dp[1<<y]=0;
		dp[1<<x]=0;
	}
	if(2*m==n*(n-1)){
		cout<<0;return;
	}
	int maxx=1<<n;
	for(int i=1;i<maxx;i++){
		for(int j=0;j<n;j++){
			if(!(i&(1<<j)))continue;
			int to=i|sta[j];
			if(dp[to]>dp[i]+1)dp[to]=dp[i]+1,pre[to]=i,id[to]=j;
		}
	}
	printf("%d\n",dp[maxx-1]);
	queue<int>Q;
	int u=maxx-1;
	while(u){
		if(id[u]!=-1)
		Q.push(id[u]);
		u=pre[u];
	}
	while(!Q.empty())printf("%d ",Q.front()+1),Q.pop();
}

https://www.luogu.com.cn/problem/P3959

分析:

这个题目做的太恼火了 最后还是没调出来 果断放弃

我们发现仅仅保存当前集合的点是不够的 因为我们不知道后面加入的点应该从哪个点转移过来 就很麻烦

因为要记录前面路径经过的点数 所以我们考虑一层一层的转移 记录当前层的状态 这样转移就好了 细节还挺多的

#include <bits/stdc++.h>
using namespace std;

int n,mm;

long long m[14][15];
long long w[15][4100];
long long dp[15][4100];

//1<<(i-1)表示第i位为1
//j&(j-1)表示把自己所有子集都枚举一遍
//若j是i的子集,i-j是j的补集
//1<<n==2^n
//1<<n-1==2^0+2^1...+2^(n-1)

int main()
{
    memset(dp,0x3f,sizeof(dp));
    memset(m,0x3f,sizeof(m));
    memset(w,0x3f,sizeof(w));
    scanf("%d%d",&n,&mm);

    for(int i=1;i<=n;i++)
        dp[1][1<<(i-1)]=0;
    for(int i=1;i<=mm;i++)
    {
        int x,y;
        long long z;
        scanf("%d%d%lld",&x,&y,&z);
        m[x][y]=min(z,m[x][y]);
        m[y][x]=m[x][y];
    }

    for(int i=1;i<=n;i++)
    {
        for(int k=1;k<=(1<<n)-1;k++)
        {
            for(int j=1;j<=n;j++)
            {
                if((1<<(j-1)&k)&&(!(1<<(i-1)&k)))
                {
                    w[i][k]=min(w[i][k],m[i][j]);
                }
            }
            //cout<<w[i][k]<<"   "<<i<<"   "<<k<<endl;
        }
    }

    long long ans=0x3f3f3f3f3f3f;
    for(int i=1;i<=(1<<n)-1;i++)
    {
        for(int j=i&(i-1);j!=0;j=i&(j-1))
        {
            long long nw=0;
            for(int k=1;k<=n;k++)
            {
                if(1<<(k-1)&(i-j))
                {
                    if(w[k][j]>ans)
                        nw=0x3f3f3f3f3f3f;
                    else nw+=w[k][j];
                }
            }
            for(int k=2;k<=n;k++)
            {
                dp[k][i]=min(dp[k-1][j]+nw*(k-1),dp[k][i]);
            }
        }
    }
    for(int i=2;i<=n;i++)
    {
        ans=min(ans,dp[i][(1<<n)-1]);
    }
    if(ans>=0x3f3f3f3f3f3f)
    {
        printf("0\n");
    }
    else printf("%lld\n",ans);
}
posted @   wzx_believer  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示