状压dp
状压dp
暴力
枚举每一天摸不摸鱼, 对于每一组方案, 我们都可以判断其可不可行, 从可行方案中选择快乐值总和最大的一组;
复杂度\(O(2^{20})\)
每一组方案可以用 一个长度为n的二进制串来表示; 从右到左第i个位置表示第i天摸不摸鱼(1表示, 0表示不摸)
当n=5时, 10111表示在1, 2, 3, 5天摸鱼, 第4天不摸;
n个数位的二进制数字可以和\([0, 2^n)\)内十进制转换;
时间复杂度\(O(N^22^n)\)
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
const int N = 22;
int n, a[N], l[N], h[N][N], b[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
scanf("%d", &l[i]);
for (int j = 1; j <= l[i]; j++) scanf("%d", &h[i][j]);
}
int ans = 0;
for (int i = 0; i < 1 << n; i++) {
for (int j = 1, k = i; j <= n; j++, k >>= 1) b[j] = k & 1;
bool ok = true;
for (int j = 1; j <= n && ok; j++) {
if (b[j] && l[j]) {
int cnt = 0;
for (int k = 1; k <= l[j]; k++) {
if (b[h[j][k]]) cnt++;
}
if (cnt == l[j]) ok = false;
}
}
if (!ok) continue;
int res = 0;
for (int j = 1; j <= n; j++)
if (b[j]) res += a[j];
ans = max(ans, res);
}
printf("%d\n", ans);
return 0;
}
优化
判断一组方案x可不可行的时候可以不那么麻烦
定义s[i]为第i天摸鱼时计划表组成的二进制数,计划表上第一天摸鱼那么二进制数相应位置为1
对于第i天, 只要\(s_{i} \& x \neq s_{i}\)即可
当\(s_{i}\&x=s_{i}\)时意味着x与s[i]二进制位相应位置都为1, 那么方案不可行
时间复杂度\(O(n2^{n})\)
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
const int N = 22;
int n, a[N], l[N], h[N], b[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
scanf("%d", &l[i]);
for (int j = 1; j <= l[i]; j++) {
int x;
scanf("%d", &x);
h[i] |= (1 << (x - 1));
}
}
int ans = 0;
for (int i = 0; i < 1 << n; i++) {
for (int j = 1, k = i; j <= n; j++, k >>= 1) b[j] = k & 1;
bool ok = true;
for (int j = 1; j <= n && ok; j++) {
if (b[j] && l[j] && (i & h[j]) == h[j]) ok = false;
}
if (!ok) continue;
int res = 0;
for (int j = 1; j <= n; j++)
if (b[j]) res += a[j];
ans = max(ans, res);
}
printf("%d\n", ans);
return 0;
}
考虑如何搜索, 在搜索过程中需要记录在之前的旅途中已经去过哪些城市, 现在在哪个城市, 到目前为止一共花费多少时间;
在之前的旅途中去过的城市的顺序时不关键的, 用\(f[i][j]\)表示去过城市的状态是i(i记录哪些城市去过, 哪些城市没去过), 现在在j号城市的情况下最少花费了多少时间;
用一个长度为n的二进制字符串记录; 从右到左第x个位置表示蜗蜗没去过x号城市(1表示去过, 0表示没去过)
n个数位的二进制数字可以和\([0, 2^n)\)内十进制转换;
转移时,一定是从i小的状态转移到i大的状态
答案 $$
\max_{2\leq j \leq n}{f[2^{n}-1][j]+a[j][1]}
\[前面所有城市已经不重复走过再从任意与1相通的的城市走进1
时间复杂度$O(n^{2}2^{n})$
```cpp
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
const int N = 22;
int n, a[N][N], f[1 << 18][19];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) scanf("%d", &a[i][j]);
memset(f, 0x3f, sizeof(f));
f[1][1] = 0;
// 枚举所有情况
for (int i = 1; i < 1 << n; i++) {
// 在走过i的二进制表示为1的城市后现在在j城市
for (int j = 1; j <= n; j++) {
// 只有当f[i][j]有路径时才可以继续,
// 当i的二进制位第j位为0时意味着这条路径不存在
if (f[i][j] < 1 << 30) {
// 枚举下一个到达的城市
for (int k = 1; k <= n; k++) {
// i & (1 << (k - 1)) 为0时意味着当前方案并没有走过城市k, 可以走这条路
if (!(i & (1 << (k - 1)))) {
f[i + (1 << (k - 1))][k] =
min(f[i + (1 << (k - 1))][k], f[i][j] + a[j][k]);
}
}
}
}
}
int ans = 1 << 30;
for (int i = 2; i <= n; i++) ans = min(ans, f[(1 << n) - 1][i] + a[i][1]);
printf("%d\n", ans);
return 0;
}
```
![](https://raw.githubusercontent.com/XuandYu000/picture/main/picture/202309182114110.png)
用f[i]表示现在还没有被消除的方块的状态为i(i记录哪些方块已经被消除了, 哪些方块还没有被消除)的情况下最小需要花费多少代价;
接下来枚举哪个/哪两个方块, 就可以转移了
用一个长度为n的二进制字符串记录; 从右往左第x个位置表示第x个方块有没有被消除(1表示被消除了, 0表示没有被消除);
有n个数位的二进制数字可以和一个$[0, 2^n]$内的十进制数字互相转换
转移时一定是从i小的状态转移到i大的状态
最后答案等于$f[2^n-1]$, 时间复杂度$O(n^22^{n})$
```cpp
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
const int N = 22;
int n, a[N], f[1 << 20];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(f, 0x3f, sizeof(f));
f[0] = 0;
// 枚举所有情况, 从右往左第j位表示i的二进制表示所代表的方案在消除了第j块方块
for (int i = 0; i < 1 << n; i++) {
// 枚举当前这一步要消除的方块
for (int j = 1; j <= n; j++) {
// 当前想要消除的方块没有被消
if (!(i & (1 << (j - 1)))) {
f[i + (1 << (j - 1))] = min(f[i + (1 << (j - 1))], f[i] + a[j]);
// 找第二块被消除的方块
for (int k = j + 1; k <= n; k++) {
if (!(i & (1 << (k - 1))))
f[i + (1 << (j - 1)) + (1 << (k - 1))] = min(
f[i + (1 << (j - 1)) + (1 << (k - 1))], f[i] + (a[j] ^ a[k]));
}
}
}
}
printf("%d", f[(1 << n) - 1]);
return 0;
}
```
#### 优化
对于一个状态i, 优先消除编号最小的没有被消除的方块
时间复杂度$O(n2^n)$
```cpp
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
const int N = 22;
int n, a[N], f[1 << 20];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(f, 0x3f, sizeof(f));
f[0] = 0;
// 枚举所有情况, 从右往左第j位表示i的二进制表示所代表的方案在消除了第j块方块
for (int i = 0; i < 1 << n; i++) {
// 枚举当前这一步要消除的方块
for (int j = 1; j <= n; j++) {
// 当前想要消除的方块没有被消
if (!(i & (1 << (j - 1)))) {
f[i + (1 << (j - 1))] = min(f[i + (1 << (j - 1))], f[i] + a[j]);
// 找第二块被消除的方块
for (int k = j + 1; k <= n; k++) {
if (!(i & (1 << (k - 1))))
f[i + (1 << (j - 1)) + (1 << (k - 1))] = min(
f[i + (1 << (j - 1)) + (1 << (k - 1))], f[i] + (a[j] ^ a[k]));
}
// 对只在这里加个break防止重复
break;
}
}
}
printf("%d", f[(1 << n) - 1]);
return 0;
}
```
![image.png](https://raw.githubusercontent.com/XuandYu000/picture/main/picture/202309182150838.png)
对于第i天, 我们只要知道从第i天开始往左数m天每天吃不吃麦当劳就可以了
用$f[i][j]$表示第i天, 最近m天每天吃不吃麦当劳的状态为j(j记录了这m天中哪些天吃了麦当劳, 那些天没吃)的情况下快乐值最大是多少;
j用一个长度为m的二进制字符串表示, 从右往左第x位置表示第$i - (m - x)$天有没有吃麦当劳(1表示吃了, 0没吃)
$f[i][j]$可以转移到$f[i + 1][j']$
i变成了i+1, j需要先除以2;
然后枚举第i天吃不吃麦当劳即可
- 吃, 状态加$2^{m - 1}$
时间复杂度$O(n2^m)$
```cpp
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
int n, m, f[256], a[100001], v[256];
bool b[256];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 0; i < 1 << m; i++) {
int cnt = 0;
for (int j = 0; j < m; j++) {
if (i & (1 << j)) cnt++;
}
if (cnt <= m / 2) b[i] = true;
}
memset(f, -1, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; i++) {
memset(v, -1, sizeof(v));
for (int j = 0; j < 1 << m; j++) {
if (f[j] >= 0) {
v[j / 2] = max(v[j / 2], f[j]);
if (b[j / 2 + (1 << (m - 1))])
v[j / 2 + (1 << (m - 1))] =
max(v[j / 2 + (1 << (m - 1))], f[j] + a[i]);
}
}
memcpy(f, v, sizeof(f));
}
int ans = 0;
for (int i = 0; i < 1 << m; i++) {
ans = max(ans, f[i]);
}
printf("%d\n", ans);
return 0;
}
```
![image.png](https://raw.githubusercontent.com/XuandYu000/picture/main/picture/202309182229561.png)
![image.png](https://raw.githubusercontent.com/XuandYu000/picture/main/picture/202309182231004.png)
当x, y固定的时候, 使用一个$<2^m$的数字表示轮廓线(从右往左第i个位置为1表示第i列存在绿色格子, 0则不存在)
$f[x][y][k]$表示考虑到了第x行第y列的格子, 轮廓线的二进制表示为k时的方案数
![image.png](https://raw.githubusercontent.com/XuandYu000/picture/main/picture/202309182235332.png)
答案等于$f[n][m][0]$
```cpp
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <vector>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
typedef long long LL;
int n, m, f[1 << 18], v[1 << 18];
const int p = 1e9 + 7;
int main() {
scanf("%d%d", &n, &m);
memset(f, 0, sizeof(f));
f[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
memset(v, 0, sizeof(v));
for (int k = 0; k < 1 << m; k++) {
if (f[k]) {
if (k & (1 << (j - 1)))
v[k - (1 << (j - 1))] += f[k], v[k - (1 << (j - 1))] %= p;
else {
if (i != n)
v[k + (1 << (j - 1))] += f[k], v[k + (1 << (j - 1))] %= p;
if (j != m && !(k & (1 << j)))
v[k + (1 << j)] += f[k], v[k + (1 << j)] %= p;
}
}
}
memcpy(f, v, sizeof(f));
}
}
printf("%d\n", f[0]);
return 0;
}
```\]