状态压缩dp

理解

当出现NPC问题,但是题目给出的N在20左右时,一般用状态压缩dp来解决

状态压缩dp本质还是动态规划,只不过是将一些不方便表示的状态用二进制数的形式来表示。

例题

例题1

在这里插入图片描述

#include <iostream>
#include <cstdio>

using namespace std;
typedef long long ll;
int N, K;
ll dp[9][(1 << 9)][82] = {0};   //dp[i][st][j] 表示0~i行共j个国王,第i行状态为st,此时的方案数
//所求结果=sum(dp[.][.][K])
//状态转移方程:dp[i][st][j]=sum( dp[i-1][st2][K-count(st)] )
int countOne(int n) {
    int count = 0;
    while (n) {
        count += (n & 1);
        n >>= 1;
    }
    return count;
}

ll f() {
    //初始化第一行,所有合法状态的方案数都是1
    for (int s = 0; s < (1 << N); s++) {
        if (s & (s << 1)) continue;
        dp[0][s][countOne(s)] = 1;

    }
    //枚举行
    for (int i = 1; i < N; i++)
        //枚举当前行状态
        for (int s = 0; s < (1 << N); s++) {
            if (s & (s << 1)) continue;
            for (int j = countOne(s); j <= K; j++) {    //0~i行的king数量
                for (int s2 = 0; s2 < (1 << N); s2++) {
                    if (s2 & (s2 << 1)) continue;
                    if (s & s2) continue;
                    if ((s << 1) & s2) continue;
                    if (s & (s2 << 1)) continue;
                    dp[i][s][j] += dp[i - 1][s2][j - countOne(s)];
                }
            }
        }
    long long ans = 0;
    for (int i = 0; i < (1 << N); i++)
        ans += dp[N - 1][i][K];
    return ans;
}

int main() {
    scanf("%d%d", &N, &K);
    cout << f();
    return 0;
}

例题2

http://acm.hdu.edu.cn/showproblem.php?pid=5418 杭电OJ

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <algorithm>
using namespace std;
const int INF = 1e9 + 7;
int T, n, m;
int dp[(1 << 16)][16]; //dp[i][j]表示,路径为i,终点为j时,最少的油耗
int oilCost[16][16];   //油耗
int f() {
    //处理特殊情况
    if (n == 1)
        return 0;
    //规定从0节点出发
    dp[1][0] = 0;
    //开始dp
    for (int st = 1; st < (1 << n); st++)    //枚举不同访问状态
        for (int i = 0; i < n; i++)
            if ((1 << i) & st)    //枚举已访问节点
                //枚举未访问节点
                for (int j = 0; j < n; j++)
                    if (!((1 << j) & st)) {
                        dp[st | (1 << j)][j] = min(dp[st | (1 << j)][j], dp[st][i] + oilCost[i][j]);
                    }
    //整理结果
    int ans = INF;
    for (int i = 1; i < n; i++) {
        ans = min(ans, dp[(1 << n) - 1][i] + oilCost[0][i]);
    }
    return ans;
}

void init() {
    //初始为INF
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (i == j)
                oilCost[i][j] = 0;
            else
                oilCost[i][j] = INF;
    while (m--) {
        int a, b, cost;
        scanf("%d%d%d", &a, &b, &cost);
        if(oilCost[a-1][b-1]<=cost)	//这一步不知道为什么,但是不加就不通过oj
            continue;
        oilCost[a - 1][b - 1] = cost;
        oilCost[b - 1][a - 1] = cost;
    }
    //floyd计算任意两点之间的最少油耗
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j < n; j++)
            for (int k = 0; k < n; k++)
                if (oilCost[i][k] + oilCost[k][j] < oilCost[i][j]) {
                    oilCost[j][i] = oilCost[i][k] + oilCost[k][j];
                    oilCost[i][j] = oilCost[i][k] + oilCost[k][j];
                }
    //打印floyd结果
//    for (int i = 0; i < n; i++) {
//        for (int j = 0; j < n; j++)
//            cout << oilCost[i][j] << " ";
//        cout << endl;
//    }

    //重置dp
    for (int i = 0; i < (1 << n); i++)
        for (int j = 0; j < n; j++)
            dp[i][j] = INF;
}

int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d%d", &n, &m);
        init();
        cout << f()<<endl;
    }
    return 0;
}

例题3

在这里插入图片描述

#include <iostream>
#include <cstdio>

using namespace std;
int count = 0;
const int Mod = 1e9;
int M, N;
int fieldState[12] = {0};
int dp[12][(1 << 12)];    //dp[i][st]表示第i行状态为st时,0~i行的方案数
void init() {
    cin >> M >> N;
    int t;
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            scanf("%d", &t);
            fieldState[i] <<= 1;
            fieldState[i] += t;
        }
    }
}

bool checkLine(int st, int landLimit) {
    return !(st & (st << 1)) && (landLimit | st) == landLimit;
}

bool checkTwoLines(int st1, int st2) {
    return !(st1 & st2);
}

int f() {
    //初始化dp第一行
    for (int st = 0; st < (1 << N); st++)
        if (checkLine(st, fieldState[0]))
            dp[0][st] = 1;
    //开始dp
    for (int i = 1; i < M; i++)
        for (int st1 = 0; st1 < (1 << N); st1++)
            if (checkLine(st1, fieldState[i]))
                for (int st2 = 0; st2 < (1 << N); st2++)
                    if (checkLine(st2, fieldState[i - 1]) && checkTwoLines(st1, st2)) {
                        dp[i][st1] = (dp[i][st1] + dp[i - 1][st2]) % Mod;
                    }
    //统计结果
    int ans = 0;
    for (int st = 0; st < (1 << N); st++) {
        ans = (ans + dp[M - 1][st]) % Mod;
    }
    return ans;
}

int main() {
    init();
    cout << f();
    return 0;
}

例题4

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;
/*写题目之前先关注:数据大小,所用方法复杂度,边界情况*/
//@Todo 为什么记两行,因为枚举当前行的状态时,要与前两行进行比较
int N, M;
int battle[100];   //记录每一行的战场情况
int goodSt[1000];   //记录在一行中合法的状态
int goodN = 0;  //一行中合法状态的数量
int dp[100][(1 << 10)][1 << 10] = {0};    //dp[i][st1][st2]表示,第i行情形为st1,i-1行情形为st2,此状态下0~i行最多能摆放的炮兵数量
//状态转移方程:dp[i][st1][st2]=max(自身,dp[i-1][st2][st3]+count(st1) ) if( checkLine(st1,st2,st3) )
void init() {
    cin >> N >> M;
//    scanf("%d%d",&N,&M);
    //预处理,将一行中的合法状态存入goodSt中
    for (int st = 0; st < (1 << M); st++) {
        if (!(st & (st << 1)) && !(st & (st << 2)))
            goodSt[goodN++] = st;
    }
    //输入战场情况,1表示平原可摆放
    char c;
    for (int i = 0; i < N; i++) {
        int battle_st = 0;
//        getchar();
        for (int j = 0; j < M; j++) {
            cin>>c;
//            scanf("%c", &c);
            battle_st <<= 1;
            battle_st += (c == 'P');
        }
        battle[i] = battle_st;
    }
}

//检查是否与当前行匹配
bool checkLine(int st, int line) {
    return (line | st) == line;
}

//检查两行之间是否匹配
bool check(int st1, int st2) {
    return !(st1 & st2);
}

//计算一种摆放状态下的部队数
int count(int st) {
    int num = 0;
    while (st) {
        num += (st & 1);
        st >>= 1;
    }
    return num;
}

//处理dp
void f() {
    if (N == 0)
        return;
    //处理第一行
    for (int i = 0; i < goodN; i++) {
        int st = goodSt[i];
        if (checkLine(st, battle[0])) {
            for (int st2 = 0; st2 < (1 << M); st2++)   //枚举-1行状态,其实并不存在,只是配合dp而用
                dp[0][st][st2] = count(st);
        }
    }
    if (N == 1)
        return;
    //处理第二行
    for (int i = 0; i < goodN; i++) {
        int st1 = goodSt[i];
        for (int j = 0; j < goodN; j++) {
            int st2 = goodSt[j];
            if (!check(st1, st2))
                continue;
            dp[1][st1][st2] = count(st1) + count(st2);
        }
    }
    //开始动态规划
    for (int row = 2; row < N; row++) {
        for (int i = 0; i < goodN; i++) { //枚举当前行摆放方式
            int st1 = goodSt[i];
            if (!checkLine(st1, battle[row]))
                continue;
            for (int j = 0; j < goodN; j++) {   //枚举上一行摆放方式
                int st2 = goodSt[j];
                if (!checkLine(st2, battle[row - 1]))
                    continue;
                for (int k = 0; k < goodN; k++) {   //枚举上上行摆放方式
                    int st3 = goodSt[k];
                    if (!checkLine(st3, battle[row - 2]))
                        continue;
                    if (!check(st1, st2) || !check(st1, st3) || !check(st2, st3))
                        continue;
                    dp[row][st1][st2] = max(dp[row][st1][st2], dp[row - 1][st2][st3] + count(st1));
                }
            }
        }
    }
}

//计算结果
int getRes() {
    f();
    //统计结果
    int ans = 0;
    for (int i = 0; i < goodN; i++)
        for (int j = 0; j < goodN; j++)
            ans = max(ans, dp[N - 1][goodSt[i]][goodSt[j]]);
    return ans;
}

int main() {
    init();
    cout << getRes();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我们来看一个具体的例子。假设有一个长度为 n 的数组 A,其中每个元素都是 0 或 1,现在需要求出所有长度为 k 的子串中,元素为 1 的个数的最小值。 传统的动态规划方法需要使用二维数组来记录状态,时间复杂度为 O(nk),空间复杂度为 O(nk)。而使用状态压缩dp,我们可以将状态压缩为一个长度为 n 的二进制数 i,其中第 j 位为 1 表示 A[j] 在当前子串中出现了一次或多次,为 0 则表示没有出现。因此,我们只需要使用一个一维数组 f 来记录当前状态的最小值即可。 具体实现如下: ```python def min_ones_in_k_substrings(A, k): n = len(A) f = [float('inf')] * (1 << n) f[0] = 0 for i in range(n): for j in range(1 << i): if bin(j).count('1') == k: ones = bin(j & ((1 << i) - 1)).count('1') + A[i] f[j] = min(f[j], f[j & ~(1 << i)] + ones) return f[(1 << n) - 1] ``` 其中,f[i] 表示状态为 i 时的最小值,初始化为正无穷。在状态转移时,我们枚举当前状态的所有子集 j,如果 j 中的元素个数等于 k,则计算 j 中包含的所有元素为 1 的个数 ones,然后更新 f[j] 的值为 f[j] 和 f[j - {i}] + ones 中的较小值。其中,j - {i} 表示将 j 中的第 i 位(即 A[i] 对应的位置)置为 0。 最终,我们返回状态为全集时的最小值 f[(1 << n) - 1] 即可。由于状态总数为 2^n,因此时间复杂度为 O(n^22^n),空间复杂度为 O(2^n)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值