各类DP题总结
DP
1.CF1729G.Cut Substrings
类型:字符串匹配类DP
题目描述:当字符串s的字串与字符串t相匹配时,需要在s中将子串删去,问最少的删除次数以及删除后有多少种不同的字串
思路:
-
数据量只有500,考虑\(n^3\)三维dp
-
设\(dp[i][j][0/1]\)表示在i这个位置用了j次删除操作满足题目要求,当前i位置结尾时删/不删的操作次数
-
考虑转移,我们可以先预处理出\([i- m + 1,i]\)这个区间是否为目标串t;
-
如果是目标串t,v[i] = 1,
-
转移1:\(dp[i][j][1] = dp[i - m][j - 1][1] + dp[i-m][j-1][0]\)
-
就代表了删去这个部分,利用的是i-m部分的j-1次
-
转移0:\(dp[i][j][0] = \sum_{k = i - m + 1}^{i-1}dp[i][j][1]\)
-
因为我现在不删那我肯定要在一个阶段里面删,那么肯定在区间\([i- m + 1,i - 1]\)里面找
如果不是目标串t,v[i] = 0,那么我直接从上面状态转移即可,我只能转移不删的状态,即\(dp[i][j][0] = dp[i - 1][j][0] + dp[i - 1][j][1]\)
-
代码:
//懒得取模拿了个自动取模板子
const int mod = 1e9+7;
struct MyLL
{
LL x;
MyLL():x(0){}
MyLL(LL u):x(u){}
LL qmi(LL a,LL b){
LL res = 1;
while(b){
if(b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
MyLL& inv(){
x = qmi(x,mod - 2);
return *this;
}
MyLL pow(LL k){
MyLL z;
z.x = qmi(x,k) % mod;
return z;
}
MyLL& operator+=(const MyLL& y){
x = (x + y.x) % mod;
return *this;
}
MyLL& operator-=(const MyLL& y){
x = ((x - y.x) % mod + mod) % mod;
return *this;
}
MyLL& operator*=(const MyLL& y){
x = (x * y.x) % mod;
return *this;
}
MyLL& operator/=(const MyLL& y){
x = (x * qmi(y.x,mod - 2)) % mod;
return *this;
}
bool operator<(const MyLL& y) const {
return x < y.x;
}
bool operator==(const MyLL& y) const {
return x == y.x;
}
bool operator>(const MyLL& y) const {
return x > y.x;
}
bool operator!=(const MyLL& y) const {
return x != y.x;
}
friend std::ostream& operator<<(std::ostream& os,const MyLL& y){
os << y.x;
return os;
}
friend std::istream& operator>>(std::istream& is,MyLL& y){
is >> y.x;
return is;
}
};
MyLL operator+(MyLL x,MyLL y){
x += y;
return x;
}
MyLL operator-(MyLL x,MyLL y){
x -= y;
return x;
}
MyLL operator*(MyLL x,MyLL y){
x *= y;
return x;
}
MyLL operator/(MyLL x,MyLL y){
x /= y;
return x;
}
const int MOD = 1e9+7;
const int N = 605;
int v[N];
MyLL dp[N][N][2];
int n,m;
void solve(){
memset(dp,0,sizeof dp);
memset(v,0,sizeof v);
string s,t;
cin >> s >> t;
n = s.size(),m = t.size();
s= " " + s,t = " " + t;
for(int i = m;i <= n;i++){
bool f = true;
for(int j = i - m + 1,l=1;j<=i;j++,l++){
if(s[j]!=t[l]){
f=false;
break;
}
}
v[i] = f;
}
dp[0][0][0] = 1;
for(int i = 1;i <= n;i++){
for(int j = 0;j <= n;j++){
if(v[i]){
if(j) dp[i][j][1] = dp[i - m][j - 1][1] + dp[i-m][j-1][0];
for(int k = i - m + 1;k < i;k++) dp[i][j][0] += dp[k][j][1];
}else{
dp[i][j][0] = dp[i - 1][j][0] + dp[i-1][j][1];
}
}
}
for(int j = 0;j <= n;j++){
if(dp[n][j][1]>0 || dp[n][j][0]>0){
cout << j << " " << dp[n][j][1] + dp[n][j][0] << endl;
return;
}
}
}
2.CF1987D.World is Mine
类型:博弈结合思维贪心的DP
题目描述:A,B吃蛋糕,A先手,第一次可以吃任意蛋糕,接着后面吃的蛋糕的甜蜜度都得严格高于之前吃的甜蜜度的最大值,然后B再吃,B可以吃任何的蛋糕;
B希望A吃的尽可能少,A希望尽可能多吃
求两者都最优吃,A能吃多少个
思路:
-
首先读题可知A每个甜蜜度只能吃一个蛋糕,a肯定从小到大吃最优
-
显然将每个相同甜蜜度的蛋糕的数量记数组\(c[i]\),那么我们发现,如果B要防止A吃当前这个x的甜蜜度的蛋糕,那么B要在A吃到这个甜蜜度蛋糕之前,把\(c[x]\)这么多x的甜蜜度的蛋糕先吃光(不然a随便吃一个就行)
-
这里就显然发现了一些策略,我b对于同一甜蜜度的蛋糕要么全部吃光,要么不吃光,如果吃一半肯定是不优
-
那么就放现是选和不选的问题
-
那么设计一下状态:
-
首先第一维度是蛋糕甜蜜度个数\(i\),表示前\(i\)个甜蜜度的蛋糕
-
自然的推出\(j\)为B吃了多少个甜蜜度种类的蛋糕(注意不是蛋糕总数,而是甜蜜度数量)
-
\(dp[i][j]\)表示前i个甜蜜度的蛋糕,B吃了\(j\)个种类蛋糕的????
-
再思考一下,dp是最大化/最小化/计数,我们要干啥,我们要让B吃的种类数尽可能多对吧,那么种类数我是\(j\)已经固定了,那么在同样吃\(j\)种类的轮次不同,就是我B可能在第三轮就吃完了j个蛋糕,也可能在第五轮吃完了j个蛋糕,那肯定是第三轮就吃完更优,所以状态出来了
-
\(dp[i][j]\)表示前i个甜蜜度的蛋糕,B吃了\(j\)个种类蛋糕的最小轮数(还是不理解的可以想象成最小总蛋糕数,因为一轮大家都会吃一个蛋糕,直到A吃不了)
-
转移就很简单:
-
\(dp[i][j] = min(dp[i-1][j-1] + c[i],dp[i- 1][j])\) 吃或不吃
-
当然\(i - j >= dp[i-1][j-1] + c[i]\)时候才可以吃,\(i-j\)为A吃到i的时候需要多少轮
3.CF1875D.Jellyfish and Mex
类型:线性dp
题目描述:给你一个长度为n的数列,你要删n次,每次删除后,把删除后的mex加到答案里,问你答案最小是多少
思路:
- 首先我们对每个数字记桶ci,发现要删一个数肯定删ci次连续删除效果更优
- 开局先算整个数组的mex,我们只对 <= mex的数进行操作
- 从大到小mex->0
- 设\(dp[i]\) 为mex为i时候的最小代价,那么\(dp[j] = min(dp[j],dp[i] + (c[j]-1) * i + j)\)
代码:
const int N = 200005;
int dp[5010];
void solve(){
map<int,int> h;
int n;
cin >> n;
for(int i = 1;i<=n;i++){
int t;
cin >> t;
h[t]++;
}
int mx = 0;
memset(dp,0x3f,sizeof dp);
for(int i = 0;i <= n;i++){
if(!h.count(i)) {
mx = i;
break;
}
}
dp[mx] = 0;
for (int i = mx; i >= 0; i--){
for (int j = i; j >= 0; j--){
dp[j] = min(dp[j], dp[i] + (h[j]-1)*i + j);
}
}
cout << dp[0] << endl;
}
4.CF1956D.Nene and the Mex Operator
类型:区间DP
题目描述:给你一个长度最多为18的数组,你可以操作最多不超过\(5\cdot10^5\)次,每次操作你可以选一个区间\([l,r]\)使得区间内的值改为该区间的\(mex\)
求数组最后通过变化最大能变成多大,并写出操作过程
思路:
-
显然贪心不行,因为无法证明如果变mex才优
-
我们思考dp,我们操作次数很多,我们可以使一些数慢慢变小,然后递增一样变大比如说 2 2 2我可以先 变成 0 1 2,然后总的求一次mex变成 3 3 3,这样明显更优,那有没有方式这样构造呢
-
当然是有,我们观察区间\([l,r]\)最大变成的mex也就是\(r-l+1\)也就是区间长度
-
例如 2 2 2我们先把他变成 0 0 0方便操作,然后看接下来的演示
-
0 0 0 -> 1 0 0 -> 2 2 0 -> 0 2 0 -> 1 2 0 -> 3 3 3
-
发现我们可以递归式的去求,假设我要最后一个数字搞出3那么我就要先搞2,如果要搞2那么前面得是1,很明显的递归,当这个例子最后第二个数字是3的时候最后第二个前数字也应该是3, 比如说数组变成了 3 3 3 0,那么怎么把最后变成4呢 很明显要对前面两个3再进行清零重新变成 1 2组合 组成 1 2 3 0才可以变成4,这点是本题关键,读者需好好理解,多推几个例子
-
所以很明显的区间dp,枚举分割点即可,如果一个大区间可以由两个小区间更优变成,那么就要去看小区间有没有进行mex操作啦,因为大区间单独就不用mex操作,这样我们可以用个\(g[l][r]\)记录每对\([l,r]\)的分割点,如果没有分割点,那就说明肯定用了mex,直接进行递归求解即可,不然就分裂
代码:
const int N = 200005;
int dp[30][30];
int g[30][30];
int n;
int a[N];
int pre[N];
vt<PII> res;
int mex(int l,int r){
for(int i = 0;i<=18;i++){
bool f = true;
for(int j = l;j <= r;j++){
if(a[j]==i){
f=false;
break;
}
}
if(f) return i;
}
return 19;
}
void add(int l,int r){
res.pb({l,r});
fill(a + l,a+r+1,mex(l,r));
}
void gt(int l,int r){
if(l > r) return;
if(l==r){
while(a[l]!=1) add(l,r);
return;
}
//逐渐 0 0 0 -> 1 0 0 -> 2 2 0 - > 0 2 0 -> 1 2 0
//从0变全部是len然后保留len个,将前len-1个重构
gt(l,r-1),add(l,r),add(l,r-1),gt(l,r-1);
}
void ptr(int l,int r){
if(g[l][r] == 0){
if(pre[r]-pre[l - 1] < (r-l+1)*(r-l+1)){
while(accumulate(a + l,a+r + 1,0ll)!=0) add(l,r);
gt(l,r-1),add(l,r);
}
return;
}
ptr(l,g[l][r]),ptr(g[l][r]+1,r);
}
void solve(){
cin >> n;
for(int i = 1;i <= n;i++) cin >> a[i],pre[i] = pre[i - 1] + a[i];
for(int len = 1;len <= n;len++){
for(int l = 1;l + len - 1 <= n;l++){
int r = l + len - 1;
dp[l][r] = max(pre[r] - pre[l - 1],len*len);
for(int k = l;k<r;k++){
if(dp[l][k] + dp[k+1][r] > dp[l][r]){
dp[l][r] = dp[l][k] + dp[k+1][r];
g[l][r] = k;
}
}
}
}
ptr(1,n);
cout << dp[1][n] << " " << res.size() << endl;
for(auto [k,v]:res){
cout << k << " " << v <<endl;
}
}

浙公网安备 33010602011771号