状压dp

发布时间 2023-12-21 19:08:18作者: viewoverlook

状压dp

image.png

暴力

枚举每一天摸不摸鱼, 对于每一组方案, 我们都可以判断其可不可行, 从可行方案中选择快乐值总和最大的一组;

复杂度\(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;
}

image.png

考虑如何搜索, 在搜索过程中需要记录在之前的旅途中已经去过哪些城市, 现在在哪个城市, 到目前为止一共花费多少时间;

在之前的旅途中去过的城市的顺序时不关键的, 用\(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; } ```\]