啊怎么前面的都没写就直接状压了啊
状压比较简单先讲状压(其实比较像深搜的优化)
可能蓝书上的例题偏难先将一些简单的
不会位运算就gg了吧
1.例题引入
首先,有很多问题由于空间限制或其他原因,我们不能通过原来的方式直接存储,而聪明的人类就把他们压缩到一个很小的单位去计算,用这种方式去操作的就有大名鼎鼎的哈希算法,还有状压算法
为什么需要状压dp?
状压dp可以解决一些题目的状态随阶段增长而增长的题目,比如一个量用了还是没用等等,把状态压缩成数字存入数组然后进行dp
啊听着好抽象啊
那我们搞一个例题看看
简化后的题目意思是有一块矩阵,可以取一些1,但是不能有相邻的1,问有多少种取法
此题是否可以 d f s dfs dfs?应该是可以的,但是会TLE
我们发现在 d f s dfs dfs的是后需要记录当前位置取没取,这样才能判断该位置可不可以去
这提示我们可以使用状压 d p dp dp
我们首先预处理出每一行有哪些取的方案合法,即不能取0也不能有相邻
不能取0比较简单,也就是把每行的草地情况压成int(存到 f f f数组),然后如果枚举到一个状态 s t a sta sta,如何判断这个 s t a sta sta没有零?就是他与该行的 f f f的且是否还是原数,因为如果不是原数,说明他选取了一些0,否则肯定是合法的
那如何判断一个状态没有相邻的?这个就要考察位运算的功底了,其实就是左移一位和右移一位都与原数没有交
预处理出这些就可以进入最后的 d p dp dp环节
d p [ i ] [ s t a ] dp[i][sta] dp[i][sta]表示第 i i i行现在的状态为 s t a sta sta
然后枚举之前的状态为 m a s k mask mask,如果 s t a sta sta与 m a s k mask mask没有交,则把 d p [ i − 1 ] [ m a s k ] dp[i-1][mask] dp[i−1][mask]的值加到 d p [ i ] [ s t a ] dp[i][sta] dp[i][sta]里面去
答案就是 ∑ d p [ n ] [ s t a ] \sum{}dp[n][sta] ∑dp[n][sta]
我们发现如果第 i i i行的取法只与 i − 1 i-1 i−1行和他自己有关,于是可以把 i i i这一维滚动掉
时间复杂度 O ( n ∗ 2 n ∗ 2 n ) O(n*2^n*2^n) O(n∗2n∗2n)
n n n才12,正好
const int p = 100000000 ;
int dp[13][5000],a[13][13],f[13],ok[5000];
int n,m ;
int main(){
scanf("%d%d",&n,&m) ;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
scanf("%d",&a[i][j]) ;
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
f[i]=(f[i]<<1)+a[i][j] ;
for (int i=0;i<(1<<m);i++) ok[i]=((!(i&(i<<1)))&&(!(i&(i>>1)))) ;
dp[0][0]=1 ;
for (int i=1;i<=n;i++){
for (int j=0;j<(1<<m);j++) {
if (ok[j] && ((j&f[i])==j)){
for (int k=0;k<(1<<m);k++)
if (!(j&k)){
dp[i][j]=(dp[i][j]+dp[i-1][k])%p ;
}
}
}
}
int ans=0;
for (int i=0;i<(1<<m);i++) ans=(ans+dp[n][i])%p ;
printf("%d\n",ans) ;
}
如果您觉得这个题目还比较困难,不妨先做一个小练习:
Water
题目描述
有 n n n个装着水的开水瓶,要把它们中的水汇总到不超过 k k k个开水瓶里,把第 i i i个开水瓶里的水全都倒到第 j j j个开水瓶里(无论它们现在装了多少水)的代价是 c i j c_{ij} cij。求最小总代价。
输入格式
第一行两个整数 n , k n,k n,k 。
接下来的 n n n 行,每行 n n n 个整数表示 c i j c_{ij} cij ,保证 c i i = 0 c_{ii}=0 cii=0 。
输出格式
输出一行一个整数表示答案。
样例
input
5 2
0 5 4 3 2
7 0 4 4 4
3 3 0 1 2
4 3 1 0 5
4 5 5 5 0
output
5
数据范围
1 ≤ k ≤ n ≤ 20 , c i j ≤ 1 0 5 1≤k≤n≤20,c_{ij}≤10^5 1≤k≤n≤20,cij≤105 。
如果您上一个题目听懂了,这个就是小case了
d p [ m a s k ] dp[mask] dp[mask]表示瓶子状态为 m a s k mask mask最小需要花费的代价
然后枚举把 i i i号瓶子的水转移到 j j j号瓶子就好了
然后如果中间1的个数 < = k <=k <=k的话就更新答案
时间复杂度 O ( n 2 ∗ 2 n ) O(n^2*2^n) O(n2∗2n)
int n, k, ans = iinf ;
int dp[N], a[25][25] ;
int calc(int x) {
int res = 0 ;
while (x) {
x = x & x - 1 ;
res++ ;
}
return res ;
}
signed main(){
scanf("%d%d", &n, &k) ;
if (n == k) print(0) ;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &a[i][j]) ;
for (int i = 0; i <= (1 << n) - 1; i++) dp[i] = iinf ;
dp[(1 << n) - 1] = 0 ;
for (int s = (1 << n) - 1; s >= 0; s--) { // 枚举当前状态
if (calc(s) <= k) ans = min(ans, dp[s]) ;
for (int i = 0; i < n; i++) // 把第i个水壶中的水
if (s & (1 << i))
for (int j = 0; j < n; j++) // 转移到第j个水壶中
if (i != j) {
dp[(s - (1 << i)) | (1 << j)] = min(dp[(s - (1 << i)) | (1 << j)], dp[s] + a[i][j]) ;
}
}
printf("%d\n", ans) ;
}
2.算法特点
能够通过状压dp解决的题目的数据基本都有一个共同点:就是数据范围很小,都是10~20差不多的
基本上都是要求记录没一个变量的取值情况什么的
还有什么呢,也就是可以 d p dp dp 呗φ(>ω<*)
3.题目选讲
先把蓝书上的题目刷了在看下面呗
宝藏
noip题,是状压dp第一次也是唯一一次出现在noip赛场上
数据范围提示性很强
我们对于每个点做一次最短路,求出 d i s dis dis表示每个点的距离
然后再改一个 d p [ i ] dp[i] dp[i]数组表示当前选择情况为 i i i的最优方案代价
然后通过 d f s dfs dfs去记忆化搜索搜出答案
答案就是 min s = 1 n d p [ 2 n − 1 ] \min_{s=1}^ndp[2^n-1] mins=1ndp[2n−1]
const int N = 15;
const int M = (1<<15) ;
int g[N][N] ;
int dp[M] ;
int dis[N] ;
int n,m ;
void dfs(int x){
for (int i=1;i<=n;i++){
if (x&(1<<(i-1))){
for (int j=1;j<=n;j++){
if ((x&(1<<(j-1)))==0 && g[i][j]!=inf){
if (dp[x]+dis[i]*g[i][j]<dp[x|(1<<(j-1))]){
int s=dis[j] ;
dis[j]=dis[i]+1;
dp[x|(1<<(j-1))]=dp[x]+dis[i]*g[i][j] ;
dfs(x|(1<<(j-1))) ;
dis[j]=s ;//重置
}
}
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof(g)) ;
for (int i=1;i<=m;i++){
int x,y,z ;
scanf("%d%d%d",&x,&y,&z) ;
g[x][y]=min(g[x][y],z) ;
g[y][x]=min(g[y][x],z) ;
}
int ans=inf ;
for (int i=1;i<=n;i++){
memset(dp,0x3f,sizeof(dp)) ;
memset(dis,0x3f,sizeof(dis));
dis[i]=1;
dp[1<<(i-1)]=0;
dfs(1<<(i-1)) ;
ans=min(ans,dp[(1<<n)-1]) ;
}
printf("%d\n",ans) ;
return 0 ;
}
出在noip也挺适合的
题意
n
n
n个问题,解决的顺序影响正确的概率,无论之前解决的问题是否答对,当前问题
j
j
j 答对概率为
m
a
x
(
a
[
i
]
[
j
]
)
max(a[i][j])
max(a[i][j]) (
i
i
i为解决过的问题)。求答对题目的最大期望和对应的答题顺序。
分析
假设当前状态是 i i i , i i i 对应的01串的1代表已经解决的问题,0代表尚未解决的,那么它肯定是由某个已解决的问题还没解决的状态转移过来的,也就是由其中一个1变为0的状态。所以我们枚举它里面的每个1, i & ( 2 l ) = 1 i\ \&\ (2^l) =1 i & (2l)=1 表示第l个数字是1, j = i − ( 2 l ) j = i-(2^l) j=i−(2l) 得到 j j j 状态。
d p dp dp表示状态i的期望, d p [ i ] = d p [ j ] + m a x ( a [ i ] [ j ] ) dp[i]=dp[j] + max(a[i][j]) dp[i]=dp[j]+max(a[i][j]) 。因为多答出一题,期望就增加(1*答出这题的概率)。
同时每个状态用 d d d储存答题顺序(期望最大且字典序最小的答题顺序)。
这句判断字典序: m x = d p [ i ] & & d [ i ] > = d [ i − ( 2 j ) ] mx=dp[i] \ \&\& \ d[i]>=d[i-(2^j)] mx=dp[i] && d[i]>=d[i−(2j)]。如果新的 d p [ i − ( 2 j ) ] dp[i-(2^j)] dp[i−(2j)]的字典序比较小, d p [ i ] dp[i] dp[i]更新。因为一开始 d d d都是0,所以要用≥,不然第一个样例就不会输出A。
int T, n ;
int a[N][N] ;
string ans[N] ;
int dp[N] ;
signed main(){
scanf("%d", &T) ;
while (T--) {
scanf("%d", &n) ;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
scanf("%d", &a[i][j]) ;
ans[0] = "" ; clr(dp) ;
for (int i = 0; i < (1 << n); i++)
for (int j = 0; j < n; j++)
if (i & (1 << j)){
int mx = 0 ;
for (int k = 0; k < n; k++) if (i & (1 << k) && a[k][j] > mx) mx = a[k][j] ;
mx += dp[i - (1 << j)] ;
char x = 'A' + j ;
if (mx > dp[i] || (mx == dp[i] && ans[i] >= ans[i - (1 << j)])) {
dp[i] = mx ;
ans[i] = ans[i - (1 << j)] + x ;
}
}
double res = dp[(1 << n) - 1] * 1.0 / 100 ;
printf("%.2lf\n", res) ;
cout << ans[(1 << n) - 1] << endl ;
}
return 0 ;
}
状态压缩dp不单单能够独自出现,还能和其他算法结合,一般具有很高的难度。
例如Password这题
这个题目真的很难
看到区间取反操作,除了线段树之外,还能够想到一种加速的方式:差分。我们将无需点亮的看做0,需要点亮的看做1,那么原序列就变成了一个01序列。我们在最后补上一个0,然后对其进行差分,这样原序列的一段1就对应了差分数组上的两个1(如果最后不补0那么原串结尾的1在差分数组上就只会有一个1,对于之后的操作就比较麻烦)
考虑每一次区间取反操作的实质,长度为K的区间取反操作在差分数组上体现为两个间隔为 K K K的数取反。显然将两个0变成1不会是最优情况,那么我们可以认为:在差分数组上对于一个0和一个1的一次区间取反操作就是1向旁边走了 K K K格,而两个1的取反操作就是两个1走到了一起然后被消除。
发现需要点亮的点最多只有10个,那么差分数组中的1最多只有20个,那么我们可以通过最短路计算差分数组上第 i i i个1和第 j j j个1走到一起的最小操作次数 f i , j f_{i,j} fi,j,然后通过状压 D P DP DP计算消除 i i i集合中的1的操作次数 g i g_i gi,大力转移即可。
int n, k, l ;
int x[N], a[N], dis[N], cost[22][22], id[N], vis[N], dp[1 << 22] ;
vector <int> g[N] ;
void add(int x, int y){
g[x].pb(y) ; g[y].pb(x) ;
}
void spfa(int s) {
clr(vis) ;
queue <int> q ;
for (int i = 0; i < N; i++) dis[i] = iinf ;
dis[s] = 0 ; vis[s] = 1 ; q.push(s) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
vis[now] = 0 ;
for (int i = 0; i < SZ(g[now]); i++) {
int to = g[now][i] ;
if (dis[to] > dis[now] + 1) {
dis[to] = dis[now] + 1 ;
if (!vis[to]) vis[to] = 1, q.push(to) ;
}
}
}
}
void douout(double x){
printf("%lf\n", x + 0.0000000001) ;
}
signed main(){
scanf("%d%d%d", &n, &k, &l) ;
clr(x) ;
for (int i = 1; i <= k; i++) {
int tmp ;
scanf("%d", &tmp) ;
x[tmp] = 1 ;
}
for (int i = 0; i <= n; i++)
if (x[i] != x[i + 1]) x[i] = 1 ;
else x[i] = 0 ;
for (int i = 1; i <= l; i++) scanf("%d", &a[i]) ;
for (int i = 0; i <= n; i++)
for (int j = 1; j <= l; j++) {
if (i - a[j] >= 0 && x[i - a[j]] == 0) add(i - a[j], i) ;
if (i + a[j] <= n) add(i, i + a[j]) ;
}
k = 0 ;
for (int i = 0; i <= n; i++)
if (x[i]) id[i] = k++ ;
memset(cost, 0x3f, sizeof(cost)) ;
for (int i = 0; i <= n; i++)
if (x[i]) {
spfa(i) ;
for (int j = 0; j <= n; j++) if (x[j]) cost[id[i]][id[j]] = dis[j] ;
}
memset(dp, 0x3f, sizeof(dp)) ;
dp[0] = 0 ;
int K = 1 << k ;
for (int i = 1; i < K; i++) {
int x, y ;
for (x = 0; x < k; x++)
if ((i >> x) & 1) break ;
for (y = 0; y <= k; y++)
if ((i >> y) & 1)
dp[i] = min(dp[i], dp[i ^ (1 << x) ^ (1 << y)] + cost[x][y]) ;
}
if (dp[K - 1] == 0x3f3f3f3f) printf("-1") ;
else printf("%d\n", dp[K - 1]) ;
}
4.作业
Hie with the Pie (状压dp入门题)
互不侵犯(中档,可以打表)
NOI2015寿司晚宴(相比CF79D简单不少,但还是值得做的)
Thanks for your reading♪(・ω・)ノ