状态压缩dp

“每天一遍,MLE再见。”

前言

对于一类题而言,如果用 $dp$ 来做的话,会有 $m$ 个状态,而作为我们来讲,我们不可能去开一个数组 $dp[2][2][2][2][2][2][2]…[2]$ 来记录,因为空间是动态的。所以我们考虑将这 $m$ 个状态压缩成一个 $m$ 位的二进制数。存储为一位,则空间复杂度与时间复杂度都是 $O(2^m)$ 的话,枚举 $[0,2^m-1]$ ,而对于这个二进制数,它的第 $k$ 位为 $1$ 则满足第 $k$ 个条件,判断语句为 if(i>>k&1) 即可。

这种题可实行的范围一般都不超过 $10 \sim 20$ ,所以很容易识别。

例题

Acwing91.最短Hamilton路径

最经典的状压题,我们用一个长度为 $n$ 的二进制数来存储,第 $k$ 位为 $0$ ,则表示第 $k$ 个点还没有到达;反之,则表示第 $k$ 个点已经到达。然后遍历即可,时间复杂度 $O(2^nn^2)$ 。

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
#define db double
typedef long long ll;
using namespace std;
const int MAXN=21;
const int MAXS=1<<21;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
int n;
int val[MAXN][MAXN],dp[MAXS][MAXN];
int main()
{
// freopen("dp-state-compression.in","r",stdin);
// freopen("dp-state-compression.out","w",stdout);
read(n);
for(int i=0;i<n;++i)
for(int j=0;j<n;++j)
read(val[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)
dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+val[k][j]);
printf("%d",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
*/

P1896 [SCOI2005]互不侵犯

也是一道较为经典的状压dp题,考虑当前行与上一行的状态,满足:

  • 没有相邻的 $1$
  • $s_{line}\&s_{line-1}=0$

即可。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
#define db double
typedef long long ll;
using namespace std;
const int MAXS=(1<<10)-1;
const int MAXN=10;
const int MAXK=101;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int N,K,Total;
ll dp[MAXK][MAXN][MAXS],ok[MAXS],cnt[MAXS];
inline void init()
{
for(int s=0;s<(1<<N);++s)
{
int o=s,c=0;
while(o)
{
if(o&1) ++c;
o>>=1;
}
cnt[s]=c;
if(!(((s<<1)|(s>>1))&s)) ok[++Total]=s;
}
dp[0][0][0]=1;
}
int main()
{
// freopen("dp-compression.in","r",stdin);
// freopen("dp-compression.out","w",stdout);
read(N),read(K);
init();
for(int line=1;line<=N;++line)
for(int l=1;l<=Total;++l)
{
int sl=ok[l];
for(int r=1;r<=Total;++r)
{
int sr=ok[r];
if(!((sr|(sr<<1)|(sr>>1))&sl))
for(int k=0;k<=K;++k)
if(k-cnt[sl]>=0)
dp[k][line][sl]+=dp[k-cnt[sl]][line-1][sr];
}
}
ll ans=0;
for(int i=1;i<=Total;++i) ans+=dp[K][N][ok[i]];
printf("%lld",ans);
return 0;
}
/*
3 2
*/

P2704 [NOI2001] 炮兵阵地

这便是一道比较进阶的状压dp题了。因为对于一排而言,我们需要记录前两行的信息才能对该行的信息进行转移。所以需要维护前两行的状态,用 $dp[i][j][k]$ 表示第 $i$ 行的状态是 $j$ 并且 $i-1$ 行的状态是 $k$ 的答案总数。并对 $i-2$ 进行转移。

但是,这样的时间复杂度是 $2^{3m}n$ ,根本不够。所以,我们需要预处理出对于每一行而言的可行方案,直接在可行方案里计算即可。

然而,空间复杂度 $O(2^{2m}n)$ ,又超出了空间限制,所以我们可以像背包那样使用滚动数组。

查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
#define db double
#define popcount(x) __builtin_popcount(x)
typedef long long ll;
using namespace std;
const int MAXN=101;
const int MAXM=11;
const int MAXS=1<<10|10;
const int INF=0x7f7f7f7f;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
int n,m,id[MAXS],sz;
int dp[2][MAXS][MAXS];
bool valid[MAXN][MAXM];
vector<int>V,okV[MAXS][MAXS];
char op[MAXM+5];
inline void init()
{
for(int s=0;s<(1<<m);++s)
{
bool flag=1;
for(int j=0;j<m-1&&flag;++j)
if(s>>j&1) flag=!(s>>(j+1)&1)&&!(s>>(j+2)&1);
if(flag) id[s]=V.size(),V.push_back(s);
}
sz=V.size(); //V[0,sz-1] is valid
for(int i=0;i<sz;++i)
for(int j=0;j<sz;++j)
{
if(V[i]&V[j]) continue;
for(int k=0;k<sz;++k)
if(!(V[i]&V[k])&&!(V[j]&V[k])) okV[i][j].push_back(k);
}
}
inline bool check(int op,int s)
{
for(int i=0;i<m;++i)
if((s>>i&1)&&!valid[op][i]) return 0;
return 1;
}
int main()
{
// freopen("dp-sp.in","r",stdin);
// freopen("dp-sp.out","w",stdout);
read(n),read(m);
init();
for(int i=1;i<=n;++i)
{
scanf("%s",op);
for(int j=0;j<m;++j) valid[i][j]=op[j]=='P';
}
/*memset(dp[0],-63,sizeof(dp[0]));*/
for(int j=0;j<sz;++j)
for(int k=0;k<sz;++k)
dp[0][j][k]=-INF;
dp[0][0][0]=0;
for(int i=1;i<=n;++i)
{
/*memset(dp[i&1],0,sizeof(dp[i&1]));*/
for(int j=0;j<sz;++j)
for(int k=0;k<sz;++k)
dp[i&1][j][k]=-INF;
for(int j=0;j<sz;++j)
for(int k=0;k<sz;++k)
{
if(dp[(i-1)&1][j][k]==-INF) continue;
for(int nxt:okV[j][k])
if(check(i,V[nxt])) dp[i&1][nxt][j]=max(dp[i&1][nxt][j],dp[(i-1)&1][j][k]+popcount(V[nxt]));
}
}
int res=0;
for(int j=0;j<sz;++j)
for(int k=0;k<sz;++k)
res=max(res,dp[n&1][j][k]);
printf("%d",res);
return 0;
}
/*
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
*/

P3959 宝藏

求一个图的最小权生成树,这里的最小权关系到其生成高度。

定义状态为 $dp[i][s]$ 表示当前生成树的高度为 $i$ ,且点集状态为 $s $ 的情况。每一次转移 $s$ 的子集 $T$ 以及 $s$ 与 $T$ 相异的部分。

判断子集的方法: T=(T-1)&s

转移为:

$dp[i][s]=\min\{dp[i-1][T]+temp\times j\}$

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
using namespace std;
const int MAXN=15;
const int MAXM=5e3+1;
const int MAXS=1<<MAXN;
const int INF=0x3f3f3f3f;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
int N,M;
ll dp[MAXN][MAXS],w,ans=INF;
ll Path[MAXN][MAXN],Dist[MAXS];
/*struct G
{
int next,to;
ll val;
G(int n=0,int t=0,ll v=0):next(n),to(t),val(v){}
}Edge[MAXM<<1];
int Head[MAXN],Total;
int Path[MAXN][MAXN];
bool Vis[MAXN];
inline void addEdge(int u,int v,ll w)
{
Edge[++Total]=G(Head[u],v,w);Head[u]=Total;
Edge[++Total]=G(Head[v],u,w);Head[v]=Total;
}
inline void Init()
{
queue<int>Q;
for(int s=0;s<N;++s)
{
memset(Vis,0,sizeof(Vis));
for(int i=0;i<N;++i) Path[s][i]=INF;
Path[s][s]=1;Vis[s]=1;
Q.push(s);
while(!Q.empty())
{
int u=Q.front();Q.pop();
Vis[u]=0;
for(int e=Head[u];e;e=Edge[e].next)
{
int v=Edge[e].to;
if(Path[s][v]>Path[s][u]+1)
{
Path[s][v]=Path[s][u]+1;
if(!Vis[v]) Q.push(v);
}
}
}
while(!Q.empty()) Q.pop();
}
}*/
int main()
{
// freopen("dp-compressin.in","r",stdin);
// freopen("dp-compressin.out","w",stdout);
read(N,M);
memset(Path,0x3f,sizeof(Path));
for(int i=1,u,v;i<=M;++i)
{
read(u,v,w);
--u,--v;
Path[u][v]=Path[v][u]=min(Path[u][v],w);
}
memset(dp,0x3f,sizeof(dp));
int S=(1<<N)-1;
for(int s=1;s<=S;++s)
for(int i=0;i<N;++i)
if((1<<i)|s==s)
{
Path[i][i]=0;
for(int k=0;k<N;++k)
if(Path[i][k]!=INF) Dist[s]|=(1<<k);
}
for(int i=0;i<N;++i) dp[0][1<<i]=0;
for(int s=2;s<=S;++s)
for(int T=s-1;T;T=(T-1)&s)
if((Dist[T]|s)==Dist[T])
{
ll res=0;
int sf=T^s;
for(int k=0;k<N;++k)
if((1<<k)&sf)
{
ll temp=INF;
for(int l=0;l<N;++l)
if((1<<l)&T) checkMin(temp,Path[l][k]);
res+=temp;
}
for(int j=1;j<N;++j)
if(dp[j-1][T]!=INF)
dp[j][s]=min(dp[j][s],dp[j-1][T]+res*j);
}
for(int i=0;i<N;++i) checkMin(ans,dp[i][S]);
/*for(int i=1,u,v;i<=M;++i)
{
read(u,v,w);
addEdge(u-1,v-1,w);
}
Init();
for(int start=0;start<N;++start)
{
ll res=INF;
for(int s=0;s<(1<<N);++s)
for(int i=0;i<N;++i)
dp[start][s]=INF;
dp[start][1<<start]=0;
for(int s=0;s<(1<<N);++s)
for(int st=0;st<N;++st)
if((s>>st)&1)
for(int e=Head[st],v;e;e=Edge[e].next)
if((s>>(v=Edge[e].to))&1)
dp[start][s]=min(dp[start][s],dp[start][s-(1<<v)]+Edge[e].val*Path[start][st]);
checkMin(ans,dp[start][(1<<N)-1]);
}*/
printf("%lld",ans);
return 0;
}
/*
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1
*/

总结

其实,状态压缩并不仅限于使用在dp内,很多地方都能用到。方便,但十分难调(机房某巨佬所言)。算是dp一环中比较难的一类了。(之后还会有四边形不等式和斜率优化)