首发于摸鱼世界
状压DP,即状态压缩DP,它的精髓在于把DP过程中的一个“状态”用一个二进制数巧妙的表示出来。接下来就从一些入门的状压题目来感受一下状压的魅力吧~
洛谷P5911 [POI2004]PRZ
大致题意:
(n)个人过最大承载(W)重量的桥,每个人有重量(w_i)与过桥时间(t_i),多人一组时间取(Max)。求最短过桥时间。
注意数据范围:(100≤W≤400,1≤t≤50,10≤w≤100,1≤n≤16)
有没有注意到(n)的数据范围很耀眼。没错,这就是我们的突破口。
先想暴力记忆化搜索,每个状态记录某些人过桥后的最小时间花费。为了记录状态,我们需要一个(bool)数组,甚至还可以瞎**回溯。
一通乱搞发现,盲目搜索就是过不了。
于是来优化吧!
每个状态我们除了用一个数组记录每个人是否加入,还有什么办法吗?
状!态!压!缩!
因为人数比较少,我们可以用一个(n)位的二进制数来记录一个状态。所以这个数转成十进制最大只有(2^{16})。可以存在数组的一维中。
考虑用(dp[i])表示状态为(i)下的最少时间花费。它可以由什么状态推导得到呢?
假设当前状态是(0111),我们可以把它拆成({0100,0011},{0011,0100})等另外两个状态。
那么如何枚举呢?这就需要熟练掌握(C++)中的位运算符。很明显,我们要先找到所有被状态(i)包含的状态(j),包含即(j)中所含的1在(i)中同一个位置一定也是1,但反过来就不一定了。
比如(1010)可以延伸到:(1010,1000,0010,0000)四个状态。
代码怎么实现呢?非常的amazing啊:for(int j=i;j>=0;j=(j-1)&i)
先给状态(j)减去一个1,再和(i)进行按位与,同时保证了不会漏枚和(j)被(i)包含。
那另外一个状态呢?进行 按位异或 就好了。比如上面的(0111⊕0100=0011),就可以把状态(i)拆成(j)与(i⊕j)两种了。
于是就有了:
(dp[i]=min(dp[i],dp[j]+t[i⊕j])(w[i]<=W))
其中(t[i])预处理为状态(i)时的时间花费,(w[i])预处理为状态(i)时的重量和,初始值(dp[i]=INF)。
于是这道题就完成了。
单一个一维表示状态的题,应该还不算难
(CODE:)
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
using namespace std;
const int N=(1<<16)+1;
int dp[N],ti[N],we[N];
int t[17],c[17];
int main()
{
int w,n;
cin>>w>>n;
for(int i=1;i<=n;i++)
scanf("%d%d",&t[i],&c[i]);
for(int i=0;i<1<<n;i++)
{
int d=i,cnt=0;
while(d)
{
cnt++;
if(d&1)
ti[i]=max(ti[i],t[cnt]),
we[i]+=c[cnt];
d>>=1;
}
dp[i]=INF;
}
dp[0]=0;
for(int i=0;i<1<<n;i++)
for(int j=i;j>=0;j=(j-1)&i)
{
if(we[i^j]<=w)dp[i]=min(dp[i],ti[i^j]+dp[j]);
if(j==0)break;
}
printf("%d
",dp[(1<<n)-1]);
return 0;
}
POJ-2441 Arrange the bulls
大致题意:
有(N)头牛和(M)个谷仓,每头牛只会选择自己喜欢的谷仓,问有多少种方案可以让每头牛都在自己喜欢的谷仓中。
其中,(1≤N,M≤20)。
一道简单的状压DP但是好像可能会MLE。但是只是练习状压DP的话是一道好题。
考虑用(dp[i][j])表示状态(i)下,有(j)头牛都选择了合适位置的方案数。状态存储谷仓的选择情况。
所以初值为(dp[0][0]=1)。枚举每一头牛(j),在(i)状态下如果他喜欢的谷仓(v)被占用了((i&1<<(v-1))),那么我们就(dp[i][j]+=dp[ihat{}1<<(v-1)][j-1]),即把原有的那头牛移开并把自己放进去,所以把原来的方案数累加上来。
最后(ans=sum_{i=1}^{2^M} dp[i][n])。
(CODE:)
#include<bits/stdc++.h>
using namespace std;
int dp[1<<20][21];
int a[21];
int s[21][21];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
for(int j=1;j<=a[i];j++)
scanf("%d",&s[i][j]);
}
dp[0][0]=1;
for(int j=1;j<=n;j++)
for(int i=1;i<1<<m;i++)
for(int k=1;k<=a[j];k++)
if(i&1<<(s[j][k]-1))dp[i][j]+=dp[i^1<<(s[j][k]-1)][j-1];
int ans=0;
for(int i=1;i<=1<<m;i++)
ans+=dp[i][n];
printf("%d
",ans);
return 0;
}
洛谷 P1278 单词游戏
大致题意:
给定(N(N≤16))个单词,任意选择其中单词,要求为后一个单词的开头必须与前一个单词的末尾字符相同。求最大能选择的单词总长度。
依然状压呗。
设(dp[i][j])表示(i)状态下以字符(j)结尾的最大单词总长度。其中状态(i)压入了这(N)个单词的选择情况。
当然,首尾字符因为只有五个,我们一一映射到1-5或者0-4是没有问题的,我为了方便,就直接把它们减去'A'作为标识就行了。记(a[j],b[j])分别标识第(j)个单词的首尾标识,(l[j])记录第(j)个单词的长度,即对答案的贡献。
再看转移方程。如果当前第(j)个单词没有被选入枚举的状态(i),那么
(dp[i+2^j][b[j]]=Max{dp[i+2^j][b[j]],dp[i][a[j]]+l[j] })
其中初始值为(dp[2^i][b[i]=l[i])
注意对于每一个(dp)值,都去对(ans)取个(Max)就好了。
(CODE:)
#include<bits/stdc++.h>
using namespace std;
const int N=20;
string x;
int a[N],b[N],l[N];
int f[1<<17][27];
int n;
int main()
{
scanf("%d",&n);
int ans=-1;
for(int i=1;i<=n;i++)
{
cin>>x;
a[i]=x[0]-'A',b[i]=x[x.size()-1]-'A',l[i]=x.length();
f[1<<i][b[i]]=l[i],ans=max(ans,f[1<<i][b[i]]);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int s=0;s<=1<<16;s++)
if(!(s&1<<j))
ans=max(ans,f[s+(1<<j)][b[j]]=max(f[s+(1<<j)][b[j]],f[s][a[j]]+l[j]));
printf("%d
",ans);
return 0;
}