题面
我们直接通过一个例题来了解子集 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[i⊕t]+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×2n,所以子集dp的复杂度用 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;
}