【算法】状态压缩dp

news/2024/7/6 7:00:00 标签: 算法

温馨提示:学习状态压缩 d p dp dp 需要有一定的位运算基础,但是只要会用就行。

第一章节 基础的状态压缩dp

先从几道例题开始讲起

luogu P4329

题意简述:
给出一个 n × n n \times n n×n 的数字网格,让你在这 n × n n \times n n×n 个数中选出 n n n 个,满足他们没有任意两个选出的位置在同一行或者同一列,问这些数的乘积最大是多少?

数据范围:
1 ≤ n ≤ 20 , 1 ≤   a i , j ≤ 100 1 \leq n \leq 20, 1 \leq\ a_{i, j} \leq 100 1n20,1 ai,j100

很显然我们可以把 满足他们没有任意两个选出的位置在同一行或者同一列 理解为每一行都分别选择一个数字,并且满足他们没有数字在同一列,求满足这个条件的最大乘积。于是此时我们可以来 d p dp dp,状态设计:
d p [ 0 / 1 ] [ 0 / 1 ] [ 0 / 1 ] . . . . . [ 0 / 1 ] [ 0 / 1 ] dp[0/1][0/1][0/1].....[0/1][0/1] dp[0/1][0/1][0/1].....[0/1][0/1]
状态里的第 i i i 维表示在我们已经选的那些行里,第 i i i 列有没有被选过, 0 0 0 表示没有, 1 1 1 表示选了。那么此时我们考虑转移:
对于 d p [ a 1 ] [ a 2 ] [ a 3 ] . . . . . [ a m − 1 ] [ a m ] dp[a_1][a_2][a_3].....[a_{m-1}][a_m] dp[a1][a2][a3].....[am1][am],我们考虑在这个基础上我们又选择了哪一列,假设其是第 j j j 列,首先需要判断 a j a_j aj 是否为 0 0 0,如果为 1 1 1 因为已经被选了,就不能再选了。如果可以,那么 d p [ a 1 ] [ a 2 ] [ a 3 ] . . . . . [ a m − 1 ] [ a m ] dp[a_1][a_2][a_3].....[a_{m-1}][a_m] dp[a1][a2][a3].....[am1][am] 显然可以转移到:
d p [ a 1 ] [ a 2 ] [ a 3 ] . . [ a j ] . . [ a m − 1 ] [ a m ] ( a j = 1 ) dp[a_1][a_2][a_3]..[a_j]..[a_{m-1}][a_m] (a_j = 1) dp[a1][a2][a3]..[aj]..[am1][am](aj=1)
即:
d p [ a 1 ] [ a 2 ] [ a 3 ] . . [ a j ] . . [ a m − 1 ] [ a m ] ( a j = 1 ) dp[a_1][a_2][a_3]..[a_j]..[a_{m-1}][a_m] (a_j = 1) dp[a1][a2][a3]..[aj]..[am1][am](aj=1) = m a x ( d p [ a 1 ] [ a 2 ] [ a 3 ] . . [ a j ] . . [ a m − 1 ] [ a m ] ( a j = 1 ) , d p [ a 1 ] [ a 2 ] [ a 3 ] . . [ a j ] . . [ a m − 1 ] [ a m ] ( a j = 1 ) × v a l u e ) max(dp[a_1][a_2][a_3]..[a_j]..[a_{m-1}][a_m] (a_j = 1), dp[a_1][a_2][a_3]..[a_j]..[a_{m-1}][a_m] (a_j = 1) \times value) max(dp[a1][a2][a3]..[aj]..[am1][am](aj=1),dp[a1][a2][a3]..[aj]..[am1][am](aj=1)×value)
这里的 v a l u e value value 是什么呢,是我们新选的第 j j j 列的那个数,他的行是什么呢,即 a 1 , a 2 , . . . . , a m a_1, a_2, ...., a_m a1,a2,....,am 1 1 1 的个数(即我们已经选择过的行数,设他为 c n t cnt cnt),因为我们是按照行的顺序来 d p dp dp 的,那么我们已经考虑了 c n t cnt cnt 行,显然我们现在正在 d p dp dp c n t + 1 cnt + 1 cnt+1 行,所以这个 v a l u e value value 就等于 a c n t + 1 ,   j a_{cnt+1,\ j} acnt+1, j
接下来是答案,答案即我们把所有的行都考虑完了,对于每一行,一共选出来了 n n n 列,我们并且知道他们没有重复的,那么显然这些列的编号是一个 1 1 1 n n n 的排列,所以答案就是 d p [ 1 ] [ 1 ] . . . . . [ 1 ] [ 1 ] [ 1 ] dp[1][1].....[1][1][1] dp[1][1].....[1][1][1]

可是发现了吗,这里的状态有 n n n,你写的过来吗?
此时我们发现,他的状态只有 0 0 0 1 1 1 两种,我们可以将这 n n n 维状态看成二进制中的位,二进制位上的 0 / 1 0/1 0/1 就表示 d p dp dp 状态里的那些 0 / 1 0/1 0/1,即把状态压缩成了一个二进制数,这就是状态压缩 d p dp dp
那么此时我们再用二进制数看看怎么来实现刚才的转移以及答案的表示。首先转移,设当前 n n n 位的状态所表示的二进制为 s s s,我们判断一个 j j j 是已经用过了即二进制中的第 j j j 位,是否是 1 1 1,那么这个就用 s > > ( j − 1 ) s >> (j - 1) s>>(j1) & 1 1 1 即可获取第 j j j 位,判断一下即可。那么对于可行的一个列 j j j ,我们来尝试转移。那么转移就是对于 s s s,设把 s s s 二进制中的第 j j j 位变成 1 1 1 后的二进制数为 s ′ s' s,那么 d p [ s ′ ] = d p [ s ] × a c n t + 1 , j dp[s'] = dp[s] \times a_{cnt+1, j} dp[s]=dp[s]×acnt+1,j,这里的 c n t cnt cnt s s s 二进制中 1 1 1 的个数,对应前面 n n n d p dp dp 里的 n n n 个维里 1 1 1 的数量。那么此时还有一个问题: s ′ s' s 怎么得到呢?很简单,显然 s ′ s' s 就是 s s s 在加上一个第 j j j 位所对应的权值,即 s + 2 j − 1 s + 2 ^ {j-1} s+2j1
接下来,答案怎么表示呢?显然 d p [ 1 ] [ 1 ] [ 1 ] . . . [ 1 ] [ 1 ] [ 1 ] dp[1][1][1]...[1][1][1] dp[1][1][1]...[1][1][1] 的二进制为 2 0 + 2 1 + 2 2 + . . . . + 2 n − 1 2^0 + 2^1 + 2^2 + .... + 2^{n-1} 20+21+22+....+2n1,这个小学数学就学过吧,这个东西等于 2 n − 1 2^n - 1 2n1,所以在压缩后的状态里即 d p [ 2 n − 1 ] dp[2^n - 1] dp[2n1]

Code:

/*
Problem ID: luogu P4329
*/
#include <bits/stdc++.h>
#define pb push_back
#define ll long long
using namespace std;
void tomin(int& x, int y){x = min(x, y);}
void tomax(int& x, int y){x = max(x, y);}
int lowbit(int x) {
    return x & (-x);
}
const int maxn = 25, maxm = 1 << 20;
double a[maxn][maxn], dp[maxm];
int n; 
int count(int x) { //普通的lowbit获取1的个数
//你也可以写朴素的方法
    int ans = 0;
    while (x) x -= lowbit(x), ans++;
    return ans;
}
int main() {
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ ) {
            cin >> a[i][j];
            a[i][j] /= 100; //这个题目里要求是百分数,你可以忽略他,不要给他误导了
        }
    dp[0] = 1;
    for (int i = 0; i <= (1 << n) - 1; i ++ ) {
        int cnt = count(i); //获取1的个数
        for (int j = 1; j <= n; j ++ )
            if (!(i >> (j - 1) & 1))
                dp[i + (1 << (j - 1))] = max(dp[i | (1 << (j - 1))], dp[i] * a[cnt + 1][j]); //此处你可以写i + (1 << (j - 1)) 也可以写 i | (1 << (j - 1)),但是平时建议你写 i | (1 << (j - 1))
    }
    printf("%.6lf", dp[(1 << n) - 1] * 100); //注意:*100也是题目里的要求,你也可以忽略他
    //输出对应状态
    return 0;
}

我们在写一道状态压缩 d p dp dp 的水题练练手。

luogu P1433

题意简述:
平面上有 n n n 个点,你最开始在 ( 0 , 0 ) (0, 0) (0,0),你需要到达所有的点,问这个最短距离。

这道题也是一道经典的状态压缩 d p dp dp 水题。
首先我们设计状态,因为我们关心的是当前我已经走了哪些点了以及我现在在哪个点,所以我们把这两个东西都考虑进我们的状态里就行了。所以由此我们可以推出状态:
d p [ s ] [ n o w ] dp[s][now] dp[s][now] s s s 表示当前二进制状态,表示每一个位置是否已经经过了, n o w now now 则表示当前我在哪一个点,那么显然答案就是 m i n ( d p [ 2 n − 1 ] [ i ] ) min(dp[2^n - 1][i]) min(dp[2n1][i])
接下来考虑转移:
对于 d p [ s ] [ j ] dp[s][j] dp[s][j],我们考虑我们下一个去哪个点,设我们去点 k k k,那么首先的条件是我们之前没有去过点 k k k 因为如果去过了,没必要浪费啊,所以我们有一个显而易见的策略,也就是不走已经走过的点。所以我们判断 s s s 二进制中的第 k k k 位是否为 0 0 0 如果是那么我们考虑转移方程:
s ′ s' s 为把 s s s 二进制中的第 k k k 为变成 1 1 1 后的状态,那么可以得到状态转移方程:
d p [ s ′ ] [ k ] = m i n ( d p [ s ′ ] [ k ] , d p [ s ] [ j ] + d i s ( j , k ) ) dp[s'][k] = min(dp[s'][k], dp[s][j] + dis(j, k)) dp[s][k]=min(dp[s][k],dp[s][j]+dis(j,k))
d i s ( j , k ) dis(j, k) dis(j,k) 即从 ( x j , y j ) (x_j, y_j) (xj,yj) 走到 ( x k , y k ) (x_k, y_k) (xk,yk) 的欧几里得距离。
这就完了? 不是的。
这道题我们需要手动初始化,也就是我们从 ( 0 , 0 ) (0, 0) (0,0) 走到 i i i 的这样的状态,也就是 d p [ 2 i − 1 ] = d i s ( 0 , i ) dp[2^{i-1}] = dis(0, i) dp[2i1]=dis(0,i) d i s ( 0 , i ) dis(0,i) dis(0,i) 就是从 ( 0 , 0 ) (0, 0) (0,0) 走到 ( x i , y i ) (x_i, y_i) (xi,yi) 的欧几里得距离。
至此,这道题就做完了,十分的简单,适合练手。

Code:

/*
Problem ID: luogu P1433
*/
#include <bits/stdc++.h>
#define pb push_back
#define ll long long
using namespace std;
void tomin(int& x, int y){x = min(x, y);}
void tomax(int& x, int y){x = max(x, y);}
const int maxn = 20, maxm = 1 << 15;
double x[maxn], y[maxn], dp[maxm][maxn];
double dis(double a, double b, double c, double d) {
    return sqrt((a - c) * (a - c) + (b - d) * (b - d));
}
int main() {
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n; cin >> n;
    for (int i = 1; i <= n; i ++ )
        cin >> x[i] >> y[i];
    for (int i = 0; i <= (1 << n) - 1; i ++ )
        for (int j = 1; j <= n; j ++ )
            dp[i][j] = 1e9;
    for (int i = 1; i <= n; i ++ ) dp[1 << (i - 1)][i] = dis(0, 0, x[i], y[i]);
    for (int i = 1; i <= (1 << n) - 1; i ++ ) {
        for (int j = 1; j <= n; j ++ ) {
            if ((i >> (j - 1) & 1)) {
                for (int k = 1; k <= n; k ++ )
                    if (!(i >> (k - 1) & 1))
                        dp[i + (1 << (k - 1))][k] = min(dp[i + (1 << (k - 1))][k], dp[i][j] + dis(x[j], y[j], x[k], y[k]));
            }
        }
    }
    double ans = 1e9;
    for (int i = 1; i <= n; i ++ )
        ans = min(ans, dp[(1 << n) - 1][i]);
    printf("%.2lf", ans);
    return 0;
}

第二章节 有后效性的状态压缩dp

对于有后效性的状态压缩 d p dp dp,我们怎么办呢 q w q qwq qwq

luogu P2622

题意简述:
n n n 栈灯,最开始全都开着,有 m m m 种开关,你可以使用他们,问最少使用多少次,可以让所有灯都关掉。
此时会发现一个比较板子的东西:我们可以把 n n n 个灯的状态记成二进制,一个位上的 0 / 1 0/1 0/1 表示关灯或者开灯,所以我们可以用数 s s s 来表示一组 n n n 个灯的状态。为了方便起见,那么我们的状态就是对于一位如果是 1 1 1 就表示这一位已经关掉了,如果是 0 0 0 就是这一位没关掉。那么最初的状态就是全都是 0 0 0 的一个二进制数,十进制中就是 0 0 0,我们用 d p [ i ] dp[i] dp[i] 表示从最初是的状态变成 i i i 这个状态的最少操作次数,那么最初的状态就是 d p [ 0 ] = 0 dp[0] = 0 dp[0]=0。可是此时你会发现,这个 d p dp dp 是有后效性的。
那么此时我们怎么解决呢?我们搭配上搜索就行了
我们把已经确定的状态存在队列里,然后我们进行 b f s bfs bfs
对于我们现在队列里的一个状态 s s s,现在我们看看他可以选择一个操作变成什么,设我们通过第 i i i 种操作得到的状态为 s ′ s' s,那么 s ′ s' s 可以由 s s s 进行一次得到,即 d p [ s ′ ] = m i n ( d p [ s ′ ] , d p [ s ] + 1 ) dp[s'] = min(dp[s'], dp[s]+1) dp[s]=min(dp[s],dp[s]+1) 如果 d p [ s ] + 1 dp[s]+1 dp[s]+1 比之前更优秀,那么我们就可以首先把他更新,然后把他放进队列里,这是因为要再次把他的后继状态更新。
最后答案就是 d p [ 2 n − 1 ] dp[2^n - 1] dp[2n1]

Code:

/*
Problem ID: luogu P2622
*/
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 15, maxm = 105, maxp = 1 << 10;
int n, m, a[maxm][maxn], cd[maxm], cq[maxm];
int dp[maxp];
bool vis[maxp];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i ++ ) {
        for (int j = 1; j <= n; j ++ ) {
            cin >> a[i][j];
            if (a[i][j] == 1)
                cd[i] |= (1 << j - 1);
            if (a[i][j] == -1)
                cq[i] |= (1 << j - 1);
        }
    }
    memset(dp, 0x3f, sizeof(dp));
    dp[0] = 0;
    queue<int> q; q.push(0);
    while (!q.empty()) {
        int t = q.front(); q.pop(); vis[t] = false;
        for (int i = 1; i <= m; i ++ ) {
            int v = ((t | cd[i]) & (~cq[i]));
            if (dp[v] > dp[t] + 1) {
                dp[v] = dp[t] + 1;
                if (!vis[v])
                    q.push(v), vis[v] = true;
            }
        }
    }
    if (dp[(1 << n) - 1] == 0x3f3f3f3f)
        cout << -1;
    else
        cout << dp[(1 << n) - 1];
    return 0;
}

至此,这就解决了。

第三章节 较复杂的状态压缩dp

本章节讲一些二维的比较复杂度的状态压缩 d p dp dp

luogu P1896

题意简述:
有一个 n × n n \times n n×n 的网格,让你在里面放 K K K 个国王,一个国王可以攻击他周围的 8 8 8 个网格,问有多少种方案可以满足不会有两个国王会互相攻击。

对于每一行的每一个位置,我们可以选择放或者不放,我们用 0 0 0 表示不放,用 1 1 1 表示放,那么显然,我们可以用一个二进制数代表一行的状态,因此我们可以设计出状态: d p [ i ] [ s ] [ k ] dp[i][s][k] dp[i][s][k] 表示现在考虑到第 i i i 行,第 i i i 行的状态是 s s s,我们已经在前 i i i 行放了 k k k 个国王的合法方案的方案数。
那么对于 s s s,我们首先要判断他是否会在一行内会有互相攻击的情况,如果有,那么这种情况一定不可能,因为这种情况一定不合法。

判断合法的方式:
我们将这个二进制数左移一位然后在与上他本身,如果>0,那么显然就有相邻的(攻击的)。因为如果>0,那么也就是说有1被左移后和原来的一个1重合了,说明他们相邻。

如果合法,那么此时我们枚举上一行的状态 s ′ s' s,判断 s ′ s' s s s s 是否会有国王起冲突。如果不会起冲突,那么我们进行转移:枚举我们在前 i i i 行放了 l l l 个国王,然后我们在第 i i i 行放了 c o u n t ( s ) count(s) count(s) 个国王,那么前 i − 1 i-1 i1 行就放了 l − c o u n t ( s ) l - count(s) lcount(s) 个,即 d p ( i , s , l ) + = d p ( i − 1 , s ′ , l − c o u n t ( s ) ) dp(i, s, l) += dp(i-1, s', l - count(s)) dp(i,s,l)+=dp(i1,s,lcount(s))。这里 c o u n t ( s ) count(s) count(s) 表示 s s s 二进制中 1 1 1 的个数。

判断两行合法的方式:
首先他们本身没有重合(即两个与起来等于 0 0 0),并且没有其中一个状态里的1的两边的位置在另一个状态里是 1 1 1 A A A 状态左移一位与上 B B B 状态等于0,并且 B B B 状态左移一位与上 A A A 状态等于0)

时间复杂度 O ( n 2 n 2 n K ) O(n2^n2^nK) O(n2n2nK)

复杂度爆炸!

怎么优化呢?
很显然我们在这里多次判断了一行内是否合法,所以我们那可以在最开始就预处理出来所有在一行内不会冲突的状态,显然会有很多状态被淘汰掉,设合法的数量为 t o t tot tot,时间复杂度:
O ( n t o t 2 K ) O(ntot^2K) O(ntot2K)
答案为 s u m ( d p [ n ] [ 一个合法状态 ] [ K ] ) sum (dp[n][一个合法状态][K]) sum(dp[n][一个合法状态][K])

Code:

/*
Problem ID: luogu P1896
*/
#include <bits/stdc++.h>
#define ll long long
#define pb push_bck
using namespace std;
void tomin(int& x, int y){x = min(x, y);}
void tomax(int& x, int y){x = max(x, y);}
bool check2(int s, int t) {
    return (s & t) == 0 && ((s << 1) & t) == 0 && (s & (t << 1)) == 0;
}
bool check1(int s) {
    return (s & (s << 1)) == 0;
}
const int maxn = 10, maxk = 105, maxm = 1 << 9;
int n, K, ok[maxm], tot;
ll dp[maxn][maxm][maxk];
int lowbit(int x) {
    return x & (-x);
}
int count(int x) {
    int ans = 0;
    while (x)
        x -= lowbit(x), ++ans;
    return ans;
}
int main() {
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n >> K;
    for (int i = 0; i < (1 << n); i ++ )
        if (check1(i))
            ok[++tot] = i;
    for (int i = 1; i <= tot; i ++ )
        dp[1][ok[i]][count(ok[i])] = 1;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= tot; j ++ )
            for (int k = 1; k <= tot; k ++ ) {
                if (!check2(ok[j], ok[k])) continue;
                int C = count(ok[j]);
                for (int l = K; l >= C; l--)
                    dp[i][ok[j]][l] += dp[i - 1][ok[k]][l - C];
            }
    ll ans = 0;
    for (int i = 1; i <= tot; i ++ )
        ans += dp[n][ok[i]][K];
    cout << ans;
    return 0;
}

http://www.niftyadmin.cn/n/4937113.html

相关文章

低成本搭建NAS,利用HFS进行内网穿透,实现公网访问

通过HFS低成本搭建NAS&#xff0c;并内网穿透实现公网访问 文章目录 通过HFS低成本搭建NAS&#xff0c;并内网穿透实现公网访问前言1.下载安装cpolar1.1 设置HFS访客1.2 虚拟文件系统 2. 使用cpolar建立一条内网穿透数据隧道2.1 保留隧道2.2 隧道名称2.3 成功使用cpolar创建二级…

突破Poshmark成号率低难题,防封攻略大揭秘!

说到二手交易电商平台&#xff0c;这里就不得不说一下Poshmark了。Poshmark是美国最大的二手交易电商平台&#xff0c;访客量很大&#xff0c;所以平台的月均访问量也是非常高的。 Poshmark 是成立于2011 年的美国线上电商平台&#xff0c;也被称为美版“闲鱼”。平台上的产品…

springboot邮件任务

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency> 依赖 配置文件 spring.mail.username1393087444qq.com spring.mail.password************* spring.mail.hos…

2.Model、ModelMap和ModelAndView的使用详解

1.前言 最近SSM框架开发web项目&#xff0c;用得比较火热。spring-MVC肯定用过&#xff0c;在请求处理方法可出现和返回的参数类型中&#xff0c;最重要就是Model和ModelAndView了&#xff0c;对于MVC框架&#xff0c;控制器Controller执行业务逻辑&#xff0c;用于产生模型数据…

Python Web:Django、Flask和FastAPI框架对比

原文&#xff1a;百度安全验证 Django、Flask和FastAPI是Python Web框架中的三个主要代表。这些框架都有着各自的优点和缺点&#xff0c;适合不同类型和规模的应用程序。 1. Django&#xff1a; Django是一个全功能的Web框架&#xff0c;它提供了很多内置的应用程序和工具&am…

ImportError: cannot import name ‘MutableMapping‘ from ‘collections‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

人机交互中的混合多重反馈

人机交互中态、势、感、知的混合多重反馈是指在交互过程中综合运用不同方面的反馈信息&#xff0c;包括用户态度&#xff08;态&#xff09;、行为动势&#xff08;势&#xff09;、情感体验&#xff08;感&#xff09;和认知反馈&#xff08;知&#xff09;。这种多重反馈可以…

MySQL 数据类型总结

整形数据类型 1字节 8bit 2^8256 MySQL 5.7 整数有个显示宽度 &#xff0c; 8.0 取消了 显示宽度 显示宽带为5&#xff0c;当insert的值不足5为&#xff0c;使用0 填充&#xff0c; create table test_1 ( c1 int(5) zerofill ); 浮点数据 定点数 位类型 BIT 日期和时间 t…