状压DP——子集DP

题面

我们直接通过一个例题来了解子集 DP,子集 DP 属于状压 DP 的一种。

给定一个长度不超过 n 的字符串 s,如果 s 中的一个子序列是回文,那么我们就可以从 s 中移除这个子序列,求最少经过多少步我们可以移除整个字符串 s。如:我们可以从"dqewfretd"中移除 “defed”,剩下的字符串即:“qwrt”。

题目解析

使用状压DP进行求解:
对于状态i,用 d p [ i ] dp[i] dp[i]表示最少的操作次数
当状态i
对应的子序列是回文时,dp[i]=1
对于状态i的一个子状态t,如果t也是回文序列,那么
d p [ i ] = m i n ( d p [ i ] , d p [ i ⊕ t ] + 1 ) dp[i]=min(dp[i],dp[i⊕t]+1) dp[i]=min(dp[i],dp[it]+1)
O ( 4 n + n × 2 n ) \mathcal{O}(4^n + n\times 2^n) O(4n+n×2n)
子集 dp 有一个巧妙的写法,把时间复杂度压缩到 O ( 3 n + n × 2 n ) O(3^n + n\times 2^n) O(3n+n×2n)一般来说 3 n 3^n 3n会远大于 n × 2 n , 所 以 子 集 d p 的 复 杂 度 用 n\times 2^n,所以子集 dp 的复杂度用 n×2ndp O(3^n)$

for (int i = 1; i < (1 << n); i++) {
    dp[i] = IsPalindrome(i) ? 1 : inf;  // 判断当前状态是否是回文,如果是回文则步骤数为 1
    for (int t = i; t; t = (t - 1) & i) {
        dp[i] = min(dp[i], dp[t] + dp[i ^ t]);
    }
}
cout << dp[(1 << n) - 1] << endl;

通过

for (int t = i; t; t = (t - 1) & i)

这个方式我们可以快速枚举一个状态的所有子集。

示例代码

#include <iostream>
#include <string>
using namespace std;
int dp[1 << 16];
int n;
string str;
bool IsPalindrome(int x){
    string ss="";
    int cnt=0;
    for(int i=0;i<n;i++){
        if(x&(1<<i)){
            ss+=str[i];
            cnt++;
        }
    }
    for(int i=0,j=cnt-1;i<j;i++,j--){
        if(ss[i]!=ss[j]){
            return false;
        }
    }
    return true;
}
int main() {
    cin >> n;
    cin >> str;
    for(int i=1;i<(1<<n);i++){
        dp[i]=IsPalindrome(i) ? 1 : n;
        for(int t=i;t;t=(t-1)&i){
            dp[i]=min(dp[i],dp[t]+dp[i^t]);
        }
    }
    cout<<dp[(1<<n)-1]<<endl;
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值