状压dp

引入

相信大家一定学过回溯。

在回溯的时候,我们有时候会遇到同一个状态需要访问多次的困境。

同时这些状态的计算又需要耗费我们大量的时间,显然,我们不想承担这样的后果。

于是,在遇到一个状态可以拆成子状态的情况下,我们引入状压dp

在需要以一些数据的状态作为一维来dp时,可以用状压dp。

简介

状压dp与常规dp不同的地方,在于它以二进制的方式在一维的空间里存下一些数据的状态。

举个例子,有1~n的数,我想知道当前哪些数用过了,我就用二进制来表示,这里用1表示用过。

如:1~5中,2和3用过,二进制就以01100来表示。

我们将其转换为十进制存进一维的状态里。

当然,我们并不需要让这个表示状态的数在两个进制里转来转去,这太麻烦了。

我们直接使用位运算。

(以下 位 均是从左到右表示)

位运算常用的基本操作

  • 判断当前位是否为真
if((1<<(n-i))&s==(1<<(n-i))) 或 if((1<<(n-i))|s==s))
  • 将当前位设置为真
s=s|(1<<(n-i));
  • 将当前位设置为假
int is=1<<(n-i);
if(s&is==is) s=s^is;

例题

第1题 O - Matching

有n本不同的书,编号1至n。

有n个不同的书包,编号1至n。

冬令营开始了,有n个学生,每个学生将会获得1个书包和1本书。

给出二维数组a[1...n][1...n],如果a[i][j]=1表示第i本书和第j个书包是兼容的,

若a[i][j]=0表示第i本书和第j个书包是不兼容的。

每个学生收到的1个书包和1本书必须是兼容的。

n本书和n个书包之间,有多少种不同的匹配方案。

输入格式

第一行,一个整数n. 1<=n<=21。

接下来是n行n列的二维数组a。

输出格式

一个整数,答案模1000000007。

输入/输出例子1

输入:

3

0 1 1

1 0 1

1 1 1

输出:

3

输入/输出例子2

输入:

4

0 1 0 0

0 0 0 1

1 0 0 0

0 0 1 0

输出:

1

输入/输出例子3

输入:

1

0

输出:

0

参考程序:

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

const int maxn=21;
const long long mod=1e9+7;

int n,is[maxn+5][maxn+5],U;
long long dp[maxn+5][(1<<maxn)+5];
bool vis[maxn+5][(1<<maxn)+5];

long long f(int now,int B) {
	if(!B) return 1;
	if(vis[now][B]) return dp[now][B];
	vis[now][B]=true;
	for(int i=1;i<=n;i++) {
		int b=1<<(n-i);
		if(((B&b)!=b)||(!is[now][i])) continue;
		int nb=B^b;
		dp[now][B]=(dp[now][B]+f(now-1,nb))%mod;
	}
	return dp[now][B];
}

int main() {
	cin>>n;
	U=(1<<n)-1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>is[i][j];
	cout<<f(n,U);
	return 0;
}

第2题 U - Grouping

有n只兔子,编号1至n。

给出二维数组a[1...n][1...n]其中a[i][j]表示第i只兔子和第j只兔子的兼容度,

数据保证a[i][i]=0, a[i][j] = a[j][i]。

现在你需要把这n只兔子分成若干组,使得每只兔子仅属于一个组。

当分组结束后,对于1<=i<j<=n,你将会获得a[i][j]元钱,前提是第i只兔子和第j只兔子分在了同一组。

应该如何分组,才能使得最终赚的钱最多。

输入格式
第一行,一个整数n。 1<=n<=16。

接下来是二维数组a, 其中 -1e9 <= a[i][j] <= 1e9。

输出格式

一个整数,最多能赚的钱。

输入/输出例子1

输入:

3

0 10 20

10 0 -100

20 -100 0

输出:

20

输入/输出例子2

输入:

2

0 -10

-10 0

输出:

0

输入/输出例子3

输入:

4

0 1000000000 1000000000 1000000000

1000000000 0 1000000000 1000000000

1000000000 1000000000 0 -1

1000000000 1000000000 -1 0

输出:

4999999999

参考程序

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

const int maxn=16;

int n,a[maxn+5][maxn+5],U;
long long dp[(1<<maxn)+5],sum[(1<<maxn)+5];
bool vis[(1<<maxn)+5];

long long f(int B) {
	if(!B) return 0;
	if(vis[B]) return dp[B];
	vis[B]=true;
	dp[B]=0;
	for(int i=B;i;i=B&(i-1))
		dp[B]=max(dp[B],sum[i]+f(B^i));
	return dp[B];
}

int main() {
	cin>>n;
	U=(1<<n)-1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>a[i][j];
	for(int i=1;i<=U;i++)
		for(int j=1;j<=n;j++)
			if((i|(1<<(n-j)))==i)
				for(int k=1;k<j;k++)
					if((i|(1<<(n-k)))==i) sum[i]+=a[j][k];
	cout<<f(U);
	return 0;
}

第3题 开锁

有n把锁,编号1至n。有m把钥匙,第i把钥匙的价格是p[i],第i把钥匙可以开k[i]把锁,

分别可以开第c[i][1],c[i][2],...第c[k[i]]把锁。

问你如果购买钥匙,用最少的费用把n把锁全部打开。

如果无论无何也不能把n把锁全部打开,输出-1。

输入格式
第一行,两个整数n和m。1<=n<=12, 1<=m<=1000。

接下来是描述m把钥匙的信息,第i把钥匙有两行:

第1行,两个整数: p[i]和k[i]。1<=p[i]<=100000, 1<=k[i]<=n。

第2行,k[i]个整数,依次表似乎第i把钥匙所能打开的锁的编号,从小到大给出编号。

输出格式

一个整数。

输入/输出例子1
输入:

2 3

10 1

1

15 1

2

30 2

1 2

输出:

25

输入/输出例子2

输入:

12 1

100000 1

2

输出:

-1

输入/输出例子3

输入:

4 6

67786 3

1 3 4

3497 1

2

44908 3

2 3 4

2156 3

2 3 4

26230 1

2

86918 1

3

输出:

69942

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

const int maxn=12,maxm=1000;

int n,m,cost[(1<<maxn)+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5],al[(1<<maxn)+5];

long long f(int B) {
	if(!B) return 0;
	if(vis[B]) return dp[B];
	vis[B]=true;
	dp[B]=1e10;
	for(int i=B;i;i=B&(i-1))
		if(al[i]) dp[B]=min(dp[B],f(B^i)+cost[i]);
	return dp[B];
}

int main() {
	memset(cost,0x3f3f3f,sizeof(cost));
	cin>>n>>m;
	U=(1<<n)-1;
	for(int i=1;i<=m;i++) {
		int p,k;
		cin>>p>>k;
		int b=0;
		while(k--) {
			int a;
			cin>>a;
			b|=1<<(n-a);
		}
		for(int j=b;j;j=b&(j-1))
			cost[j]=min(cost[j],p),al[j]=true;
	}
	if(f(U)<1e10) cout<<f(U);
	else cout<<-1;
	return 0;
}

第5题 不同排列

有n个学生,学号1至n。你现在需要把这n个学生从左往右排成一行形成队伍,要满足如下所有m个条件:

第i个条件的格式是x[i],y[i],z[i],表示队伍的前x[i]学生当中,学号小于y[i]的学生不能超过z[i]人。

求满足上面所有m个条件的队伍有多少种不同的方案。

输入格式

第一行,n和m。 2<=n<=18, 0<=m<=100。、

接下来m行,第i行是x[i],y[i],z[i]。1<=x[i],y[i]<n,0<=z[i]<n。

输出格式

一个整数。

输入/输出例子1

输入:

3 1

2 2 1

输出:

4

输入/输出例子2

输入:

5 2

3 3 2

4 4 3

输出:

90

输入/输出例子3

输入:

18 0

输出:

6402373705728000

参考程序

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

const int maxn=18,maxm=100;

int n,m,cnt[(1<<maxn)+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5];
struct node {
	int y,z;
};
vector<node>limit[maxn+5];
vector<int>d[(1<<maxn)+5];

bool check(int s,int len) {
	for(int i=0;i<limit[len].size();i++) {
		int y=limit[len][i].y,z=limit[len][i].z,sum=0;
		for(int j=0;j<len;j++)
			if(d[s][j]<=y) sum++;
		if(sum>z) return 0;
	}
	return 1;
}

long long f(int B) {
	if(!B) return 1;
	if(vis[B]) return dp[B];
	vis[B]=true;
	dp[B]=0;
	if(!check(B,cnt[B])) return 0;
	for(int i=1;i<=n;i++) {
		int is=1<<(n-i);
		if((B&is)!=is) continue;
		dp[B]=dp[B]+f(B^is);
	}
	return dp[B];
}

int main() {
	cin>>n>>m;
	U=(1<<n)-1;
	for(int i=1;i<=m;i++) {
		int x,y,z;
		cin>>x>>y>>z;
		limit[x].push_back({y,z});
	}
	for(int i=1;i<=U;i++) {
		for(int j=1;j<=n;j++) {
			int is=1<<(n-j);
			if((i&is)!=is) continue;
			d[i].push_back(j);
		}
		cnt[i]=d[i].size();
	}
	cout<<f(U);
	return 0;
}

第6题 序列

有两个序列:a[1...n]和b[1...n]。你需要把序列a变成序列b。

每次你可以对序列a进行如下两种操作之一:

1、选择一个下标i(1<=i<=n),你可以让a[i]减少1或者让a[i]增加1,本次费用为X。

2、选择一个下标i(1<=i<n),你可以交换a[i]和a[i+1]的值,本次费用为Y。

你可以进行任意多次上述操作,求最少的费用使得a变成b一样。

输入格式

第一行,三个整数:n,X,Y。2<=n<=18, 1<=X<=1e8, 1<=y<=1e16。

第二行,n个整数,第i个整数是a[i], 1<=a[i]<=1e8。

第三行,n个整数,第i个整数是b[i], 1<=b[i]<=1e8。

输出格式

一个整数。

输入/输出例子1

输入:

4 3 5

4 2 5 2

6 4 2 1

输出:

16

输入/输出例子2

输入:

5 12345 6789

1 2 3 4 5

1 2 3 4 5

输出:

0

参考程序

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

const int maxn=18;

int n,X,Y,a[maxn+5],b[maxn+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5];

long long f(int now,int B) {
	if(!B) return 0;
	if(vis[B]) return dp[B];
	vis[B]=true;
	int cnt=0;
	dp[B]=1e18;
	for(int i=1;i<=n;i++) {
		int is=1<<(n-i);
		if((B&is)!=is) {
			cnt++;
			continue;
		}
		long long cost=abs(b[now]-a[i])*X+abs(now-i+cnt)*Y;
		dp[B]=min(dp[B],f(now-1,B^is)+cost);
	}
	return dp[B];
}

signed main() {
	cin>>n>>X>>Y;
	U=(1<<n)-1;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=1;i<=n;i++)
		cin>>b[i];
	cout<<f(n,U);
	return 0;
}

第7题 接龙

有n个字符串,第i个字符串是S[i]。每个字符串都是由不超过10个小写英文字母构成。

现在A和B两人要玩字符串接龙游戏,A是先手。

每次当前玩家从剩下的字符串当中选中一个拿出来(不妨假设选中的是S[j]),

接龙到前面的字符串(不妨假设上一个被选出来的字符串是s[i]),

那么S[j]的首字母必须等于S[i]的末尾字母,这样才算接龙接得上。

如果轮到当前玩家了,而当前玩家却发现从剩下得字符串当中找不到能接得上龙得字符串,那么游戏结束,当前玩家输。

假设A和B都用最优策略玩游戏。

如果A能胜利输出"First",如果B能勝利輸出"Second"。

输入格式

第一行,一個整數n。 1<=n<=16。

接下來有n行,第i行是字符串S[i]。

输出格式

如果A能胜利输出"First",如果B能勝利輸出"Second"。

输入/输出例子1

输入:

6

enum

float

if

modint

takahashi

template

输出:

First

输入/输出例子2

输入:

10

catch

chokudai

class

continue

copy

exec

havoc

intrinsic

static

yucatec

输出:

Second

参考程序

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

const int maxn=16;
int n,U;
string s[maxn+5];
bool dp[maxn+5][(1<<maxn)+5][2],vis[maxn+5][(1<<maxn)+5][2];
bool ans=0;

bool f(int now,int B,int q) {
	if(!B) return true;
	if(vis[now][B][q]) return dp[now][B][q];
	vis[now][B][q]=true;
	dp[now][B][q]=false;
	bool Q=false;
	for(int i=1;i<=n;i++) {
		int is=1<<(n-i);
		if((B&is)!=is||s[now][s[now].size()-1]!=s[i][0]) continue;
		Q=true;
		dp[now][B][q]|=!f(i,B^is,!q);
	}
	if(!Q) dp[now][B][q]=true;
	return dp[now][B][q];
}

int main() {
	cin>>n;
	U=(1<<n)-1;
	for(int i=1;i<=n;i++)
		cin>>s[i];
	for(int i=1;i<=n;i++)
		ans|=f(i,U^(1<<(n-i)),1);
	if(ans) cout<<"First";
	else cout<<"Second";
	return 0;
}

重点例题:铺地砖

有高为h,宽为w的二维表格,现在要用\({1*2}\)\({2*1}\)的地砖将其铺满,有多少种不同方案?

输入格式

两个整数数字:高度h和大型矩形的宽度w。 1 < = h,w < = 16。

输出格式

一个整数,答案模1000000007。

输入/输出例子1

输入:

2 4

输出:

5

参考程序

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

const int maxn=16;
const long long mod=1000000007;

int n,m,U;
long long dp[maxn+5][maxn+5][(1<<maxn)+5];//这里状压记录的是会影响当前行的数据,即当前行前面和上一行后面的数据
bool vis[maxn+5][maxn+5][(1<<maxn)+5];

bool in(int i,int j) {
	return ((1<<(m-i))|j)==j;
}

int set0(int i,int s) {
	int x=1<<(m-i);
	if(in(i,s)) return s^x;
	else return s;
}

int set1(int i,int s) {
	int x=1<<(m-i);
	return s|x;
}

long long f(int x,int y,int s) {
	if(x>n) return 1;
	if(y>m) return f(x+1,1,s);
	if(vis[x][y][s]) return dp[x][y][s];
	vis[x][y][s]=true;
	if(!in(y,s)) {
		if(x<n) dp[x][y][s]=f(x,y+1,set1(y,s));
		if(y<m&&!in(y+1,s)) dp[x][y][s]=(dp[x][y][s]+f(x,y+1,set1(y+1,s)))%mod;
	}
	else dp[x][y][s]=f(x,y+1,set0(y,s));
	return dp[x][y][s];
}

int main() {
	cin>>n>>m;
	if((n*m)%2==1) {
		cout<<0;
		return 0;
	}
	U=(1<<m)-1;
	cout<<f(1,1,0);
	return 0;
}
posted @ 2023-04-29 07:04  ForBiggerWorld  阅读(137)  评论(0)    收藏  举报