状压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);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!