啊哈 来写一个状压dp八

状态压缩

状态压缩DP,简称状压DP。

那状态压缩是什么捏?比如我们有一个vis数组,这个数组的第ii个元素代表这个元素选或不选。

那比如编号1,4,51,4,5的元素选了,0,2,30,2,3没有选的vis数组就可以这样表示:

1
2
3
4
5
6
vis
------------------------------
|key | 0 | 1 | 2 | 3 | 4 | 5 |
|----|---|---|---|---|---|---|
|val | 0 | 1 | 0 | 0 | 1 | 1 |
------------------------------

这里vis数组的一系列权值都是0和1。只有0和1有没有让你想起什么?没错,2进制啊!!!

那如果想用一个二进制数表示这个vis数组,就可以这样:

1
2
(   1      1      0      0      1      0)
vis[5] vis[4] vis[3] vis[2] vis[1] vis[0]

那我们表示这个数组的对应的二进制数就是(010011)2(010011)_{2}。第ii个元素就对应了这个二进制数的从右往左第ii位。

至于为什么要从右往左,等你写代码你就会感到方便了。

那现在你对状态压缩是不是有点感觉了?状态压缩就是用一个二进制数来表示当前的状态,因为用数来表示操作会更快捷。

那长度为NN的一个二进制数就可以表示NN个元素的状态,范围是从002n12^n-1,所以在使用状压之前,要看看数据范围能不能容许你数达到2n12^n-1这个大小。

例题

不管怎样,先来几道状压DP的题练练手吧

旅行商

题目

题目描述

beat放假了想要去旅游,他准备去NN个城市玩,这些城市分别编号11~NN

beat从11号城市开始,想玩遍所有NN个城市,最后回到11号城市。

每个城市之间都可以坐飞机来往,从ii号城市到jj号城市(1i,jn)(1 \leq i,j \leq n)的机票是Ai,jA_{i,j}元。Ai,jA_{i,j}不一定等于Aj,iA_{j,i}

beat不想重复玩同一个城市,请帮beat规划一种方案,使他花费的钱最少,并给出最少的钱数!

数据范围

对于100100%的数据,1N181 \leq N \leq 18,如果i=ji = jAi,j=0A_{i,j} = 0, 否则1Ai,j1041 \leq A_{i,j} \leq 10^4

时间限制1秒。

样例

样例输入

1
2
3
2
0 1
2 0

样例输出

1
3

思路

对于每个城市,我们只有两种状态:去或者没去。分别可以用0011表示。

注意到NN最大到1818,非常的小,2n12^n-1最大到262144262144,然后我们还要枚举最后一次到达的城市是O(N)O(N),那O(2NN)O(2^N N)正好能过1秒的时间限制。

我们先考虑状态,dpi,jdp_{i,j}就表示当前走过的城市状态ii,最后一次到达的城市jj

然后考虑转移:

我们第一维枚举去过的城市状态iii[1,2n1]i \in [1,2^{n}-1]

我们第二维和第三维枚举最后一个去过的城市jj和下一个要去的城市kkj,k[1,n]j,k \in [1,n]

现在考虑转移,首先我们要判断两个条件,看看现在的i,j,ki,j,k能不能存在:

  1. dpi,jdp_{i,j}要存在,你连这个状态都不存在你怎么走到kk呢?我们会把dpdp里的元素全部都变成无穷大,所以只需要判断dpi,jdp_{i,j}是否小于无穷大就行了。

  2. 题目说每一个城市不能重复去,所以我们要判断当前状态ii中,城市kk有没有出现,那就是i >> (k - 1) & 100不就行了。那状态ii变成去过城市kk就是ii的第kk位要变成11,即新状态为i + (1 << (k - 1))

那不就可以写出转移方程:

dp[i + (1 << (k - 1))][k] = min(dp[i + (1 << (k - 1))][k], dp[i][j] + a[j][k])

然后我们就做完了!

代码

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
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int n, a[19][19], dp[1 << 18][19];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
memset(dp, INF, sizeof(dp));
dp[1][1] = 0;
for (int i = 0; i < (1 << n); i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
if (dp[i][j] < INF && !(i >> (k - 1) & 1)) {
dp[i + (1 << (k - 1))][k] = min(dp[i + (1 << (k - 1))][k], dp[i][j] + a[j][k]);
}
}
}
}
int ans = INF;
for (int i = 1; i <= n; i++) {
ans = min(ans, dp[(1 << n) - 1][i] + a[i][1]);
}
cout << ans;
return 0;
}

祈愿

题目

题目描述

祈愿现在有NN个角色池,编号为11NN。每个角色池的大保底为AiA_i发才能保证出当前池子的角色。

现在beat想要把所有NN个角色(其实想要skk)都抱回尘歌壶,但是他的运气实在是太差了,必须不断抽到大保底的次数才能获得当前池子的角色。

老米看他太悲惨了,所以如果他选择把两个池iijj的次数同时抽完,那只需要aia_i ^ aja_j的次数就可以大保底了。

beat想知道获得所有角色最少需要抽多少发!

数据范围

2N202 \leq N \leq 20

1Ai1051 \leq A_i \leq 10^5

样例

样例输入

1
2
3
1 4 5

样例输出

1
2

思路

首先对于每个角色池,只有可能抽过和没抽过两种状态,所以又可以用状压dp了,并不难

定义状态:dpidp_i就表示当前状态ii的最优方案。

那我们第一维先枚举状态ii,然后第二维枚举选择的第一个池jj,这里依旧要判定ii的第jj位不能为11

那这里的状态更新:

dp[i + (1 << (j - 1))] = min(dp[i + (1 << (j - 1))], dp[i] + a[j]);

题目里也可以同时抽两个池,所以要多一维kk,这里kk不能等于jjii的第kk位也不能是11

代码

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
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int n;
int a[21], dp[1 << 20];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
memset(dp, INF, sizeof(dp));
dp[0] = 0;
for (int i = 0; i < (1 << n) - 1; i++) {
for (int j = 1; j <= n; j++) {
if ((i >> (j - 1)) & 1) {
continue;
}
dp[i + (1 << (j - 1))] = min(dp[i + (1 << (j - 1))], dp[i] + a[j]);
for (int k = 1; k <= n; k++) {
if (j == k) {
continue;
}
if ((i >> (k - 1)) & 1) {
continue;
}
dp[i + (1 << (j - 1)) + (1 << (k - 1))] = min(dp[i + (1 << (j - 1)) + (1 << (k - 1))], dp[i] + (a[j] ^ a[k]));
}
}
}
cout << dp[(1 << n) - 1];
return 0;
}

// 想要skk想要skk想要skk

打游戏

题目

题目描述

需要在学校呆NN天的beat喜欢玩原神,如果第ii天他玩上了原神,他就会获得AiA_i点快乐值。

但是学习也很重要虽然没有玩原神重要,beat在连续的MM天里,最多只能有一半的天数玩原神,请帮他规划哪几天可以玩原神,然后输出他最大可以获得的快乐值!

数据范围

样例

样例输入

1
2
4 3
1 2 3 4

样例输出

1
5