区间DP

题目一 (一个字符串变成另一个字符串的最小次数)

[SCOI2003]小小粉刷匠(https://ac.nowcoder.com/acm/problem/16129)

题目描述

每一面墙有n个段,对于每个段指定一个目标颜色ci。刚开始的时候所有的墙壁为白色
我们现在有一个刷子,刷子长度为m,刷子每次可以选择一种颜色,然后选择段数为(1~m)连续的墙段刷成选择的一种颜色。
我们现在想要知道,为了把墙变成目标颜色,最少刷多少次(保证指定的目标颜色一定不为白色)。

输入描述:

对于每一个案例,我们第一行包括两个整数n,k(1<=n<=100,1<=m<=50,k<n),表示墙的长度为n,刷子的长度为m。
第二行输入n个整数(c1c2...cn),(1<=ci<=256),表示对于墙的每一段指定的颜色。

输出描述:

输出一个数,表示小名最少刷多少次。

示例1

输入
3 3
1 2 1
输出
2

示例2

输入
5 4
5 4 3 3 4
输出
3

思路

f[i][j]:把区间[i, j]刷成目标墙的最少粉刷次数
枚举k, 满足s[i]==s[k]那么可以s[i]和s[k]一起刷。
和这题相似:https://blog.nowcoder.net/n/2fc2974ec2a24ea98671f3caef92a323

if(c[L]==c[k]&&k-L+1<=m) f[L][R]=min(f[L][R], f[L+1][k]+f[k+1][R]);//,满足刷子的长度
else f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]);//否则分开刷
#include <bits/stdc++.h>
#define LL long long
using namespace std;

int c[105], f[105][105];
int main() {

    int n, m; scanf("%d%d", &n, &m);
    for(int i=1; i<=n; i++) scanf("%d", &c[i]), f[i][i]=1;
    for(int Len=2; Len<=n; Len++){
        for(int L=1; L<=n-Len+1; L++){
            int R=L+Len-1;
            f[L][R]=f[L+1][R]+1;
            for(int k=L+1; k<=R; k++){
                if(c[L]==c[k]&&k-L+1<=m) f[L][R]=min(f[L][R], f[L+1][k]+f[k+1][R]);
                else f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]);
            }
        }
    }
    printf("%d\n", f[1][n]);

    return 0;
}

题目二

[HAOI2008]玩具取名(https://ac.nowcoder.com/acm/problem/19973)

题目描述

某人有一套玩具,并想法给玩具命名。首先他选择WING四个字母中的任意一个字母作为玩具的基本名字。然后他会根据自己的喜好,将名字中任意一个字母用“WING”中任意两个字母代替,使得自己的名字能够扩充得很长。
现在,他想请你猜猜某一个很长的名字,最初可能是由哪几个字母变形过来的。

输入描述:

第一行四个整数W、I、N、G。表示每一个字母能由几种两个字母所替代。
接下来W行,每行两个字母,表示W可以用这两个字母替代。
接下来I行,每行两个字母,表示I可以用这两个字母替代。
接下来N行,每行两个字母,表示N可以用这两个字母替代。
接下来G行,每行两个字母,表示G可以用这两个字母替代。
最后一行一个长度不超过Len的字符串。表示这个玩具的名字。

输出描述:

一行字符串,该名字可能由哪些字母变形而得到。(按照WING的顺序输出)
如果给的名字不能由任何一个字母变形而得到则输出“The name is wrong!”

示例1

输入
1 1 1 1
II
WW
WW
IG
IIII
输出
IN

备注:

30%数据满足Len≤20,W、I、N、G≤6
100%数据满足Len≤200,W、I、N、G≤16

思路

用f[i][j][k]:表示区间[i, j]的字符串是否能合成字符k。

#include <bits/stdc++.h>
#define LL long long
using namespace std;

int id[5][5][5];
int f[205][205][5];
void clstr(char s[]){
    int n=strlen(s);
    for(int i=0; i<n; i++){
        if(s[i]=='W') s[i]=1;
        if(s[i]=='I') s[i]=2;
        if(s[i]=='N') s[i]=3;
        if(s[i]=='G') s[i]=4;
    }
}

int main() {

    int W, I, N, G; scanf("%d%d%d%d", &W, &I, &N, &G);
    char s[205];
    for(int i=1; i<=W; i++) scanf("%s", s), clstr(s), id[s[0]][s[1]][1]=1;
    for(int i=1; i<=I; i++) scanf("%s", s), clstr(s), id[s[0]][s[1]][2]=1;
    for(int i=1; i<=N; i++) scanf("%s", s), clstr(s), id[s[0]][s[1]][3]=1;
    for(int i=1; i<=G; i++) scanf("%s", s), clstr(s), id[s[0]][s[1]][4]=1;
    scanf("%s", s+1); clstr(s+1);

    int n=strlen(s+1);
    for(int i=1; i<=n; i++) f[i][i][s[i]]=1;

    for(int Len=2; Len<=n; Len++){
        for(int L=1; L<=n-Len+1; L++){
            int R=L+Len-1;
            for(int k=L; k<=R; k++){
                for(int w=1; w<=4; w++){
                    for(int z=1; z<=4; z++){
                        if(f[L][k][w]&&f[k+1][R][z]){
                            if(id[w][z][1]) f[L][R][1]=1;
                            if(id[w][z][2]) f[L][R][2]=1;
                            if(id[w][z][3]) f[L][R][3]=1;
                            if(id[w][z][4]) f[L][R][4]=1;
                        }
                    }
                }
            }
        }
    }
    int is=0;
    if(f[1][n][1]) is=1, printf("W");
    if(f[1][n][2]) is=1, printf("I");
    if(f[1][n][3]) is=1, printf("N");
    if(f[1][n][4]) is=1, printf("G");
    if(!is) printf("The name is wrong!");
    printf("\n");

    return 0;
}

题目三 字符串合成字符 求合并得到的最大贡献(状压+区间dp)

[HAOI2016]字符合并(https://ac.nowcoder.com/acm/problem/19997)

题目描述

有一个长度为 n 的 01 串,你可以每次将相邻的 k 个字符合并,得到一个新的字符并获得一定分数。得到的新字符和分数由这 k 个字符确定。你需要求出你能获得的最大分数。
输入描述:
第一行两个整数n,k。
接下来一行长度为n的01串,表示初始串。
接下来2^k行,每行一个字符ci和一个整数wi,ci 表示长度为k的01串连成二进制后按从小到大顺序得到的第i种合并方案得到的新字符,wi表示对应的第i种方案对应获得的分数。
1 ≤ n ≤ 300,0 ≤ ci ≤ 1,wi ≥ 1, k ≤ 8

输出描述:

输出一个整数表示答案

示例1

输入
3 2
101
1 10
1 10
0 20
1 30
输出
40

样例解释

00 合成1 得到贡献10
01 合成1 得到贡献10
10 合成0 得到贡献20
11 合成1 得到贡献30

思路

f[i][j][s]:区间[i, j]合并s串的最大贡献。
但是转移要枚举,枚举区间长度,枚举起点。枚举分割点k,枚举[L, k]的状态s1, 枚举[k+1, R]的状态s2, s1的长度,s2的长度。
复杂度我都不想计算了。

有几个可以优化的地方。
1.枚举[L, k]的长度和[k+1, R]的长度。
对于一个长度len的串最后合成的长度为(len-1)%(k-1)+1
所以区间长度确定,最后的合成长度就确定了。
2.枚举[L, k]的状态和[k+1, R]的状态。
我们只枚举[L, k]的状态,并且转移的时候只让[k+1, R]贡献最后一位。
所以[k+1, R]的长度为1, k, 2k-1, 3k-2...。

然后就可以了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
 
LL f[305][305][(1<<8)+20];
char s[305];
int id[(1<<10)];
LL w[(1<<10)];
const LL inf=-0x3f3f3f3f3f3f3f3f;
int main() {
 
    memset(f, -0x3f, sizeof(f));
    int n, k;
    scanf("%d%d", &n, &k);
    scanf("%s", s+1);
    for(int i=0; i<(1<<k); i++) {
        int a, b;
        scanf("%d%lld", &id[i], &w[i]);
    }
    for(int i=1; i<=n; i++) f[i][i][s[i]-'0']=0;
 
    for(int Len=2; Len<=n; Len++) {
        for(int L=1; L<=n-Len+1; L++) {
            int R=L+Len-1;
            int d=(Len-1)%(k-1);
            if(!d) d=k-1;
            for(int mid=R; mid>=L; mid-=k-1){//[mid, R]的长度只能为1, k, 2k1-, 3k-2, .....
                for(int s=0; s<(1<<d); s++){//枚举[L, mid-1]的状态
                    if(f[L][mid-1][s]==inf) continue;
                    if(f[mid][R][0]!=inf){
                        f[L][R][s<<1]=max(f[L][R][s<<1], f[L][mid-1][s]+f[mid][R][0]);
                    }
                    if(f[mid][R][1]!=inf){
                        f[L][R][s<<1|1]=max(f[L][R][s<<1|1], f[L][mid-1][s]+f[mid][R][1]);
                    }
                }
            }
            if(d==k-1){//长度为k
                LL g[2]={inf, inf};//中间变量
                for(int s=0; s<(1<<k); s++){
                    if(f[L][R][s]!=inf){
                        g[id[s]]=max(g[id[s]], f[L][R][s]+w[s]);
                    }
                }
                f[L][R][0]=g[0];
                f[L][R][1]=g[1];
            }
        }
    }
    LL ans=0;
    int Len=n%(k-1)?n%(k-1):k-1;
    for(int i=0; i<(1<<Len); ++i) ans=max(ans,f[1][n][i]);
 
    cout<<ans<<endl;
 
    return 0;
}

题目四 字符串折叠

[SCOI2003]字符串折叠 (https://ac.nowcoder.com/acm/problem/20238)

题目描述

折叠的定义如下:

一个字符串可以看成它自身的折叠。记作S = S
X(S)是X(X>1)个S连接在一起的串的折叠。记作X(S) = SSSS…S(X个S)。
如果A = A’, B = B’,则AB = A’B’ 例如,因为3(A) = AAA, 2(B) = BB,所以3(A)C2(B) = AAACBB,而2(3(A)C)2(B) = AAACAAACBB

给一个字符串,求它的最短折叠。例如AAAAAAAAAABABABCCD的最短折叠为:9(A)3(AB)CCD。

输入描述:

仅一行,即字符串S,长度保证不超过100。

输出描述:

仅一行,即最短的折叠长度。

示例1

输入
NEERCYESYESYESNEERCYESYESYES
输出
14

思路

f[L][R]:区间[L, R]可以合成的最短的字符串长度
枚举k为Len=R-L+1的因数,以k为循环节。

#include <bits/stdc++.h>
#define LL long long
using namespace std;

char s[105];
int f[105][105];

int ok(int L, int R, int k){
    for(int i=0; i<k ;i++){
        if(s[i+L]!=s[i+R]) return 0;
    }
    return 1;
}
int main() {

    scanf("%s", s+1);
    int n=strlen(s+1);
    memset(f, 0x3f, sizeof(f));
    for(int i=1; i<=n; i++) f[i][i]=1;
    for(int Len=2; Len<=n; Len++){
        for(int L=1; L<=n-Len+1; L++){
            int R=L+Len-1;
            for(int k=L; k<R; k++){
                f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]);
            }
            for(int k=1; k<Len; k++){
                if(Len%k==0){
                    int ff=0;
                    for(int pL=L; pL<=R-2*k+1; pL+=k){
                        if(!ok(pL, pL+k, k)){
                            ff=1; break;
                        }
                    }
                    if(!ff){
                        int x=Len/k;
                        int s=0;
                        while(x) {x/=10, s++;}
                        f[L][R]=min(f[L][R], f[L][L+k-1]+2+s);
                    }
                }
            }
            //cout<<L<<" "<<R<<"="<<f[L][R]<<endl;
        }
    }
    printf("%d\n", f[1][n]);

    return 0;
}

题目五 [SDOI2008]SUE的小球 和时间有关系的区间DP

题目描述

Sue和Sandy最近迷上了一个电脑游戏,这个游戏的故事发在美丽神秘并且充满刺激的大海上,Sue有一支轻便小巧的小船。然而,Sue的目标并不是当一个海盗,而是要收集空中漂浮的彩蛋,Sue有一个秘密武器,只要她将小船划到一个彩蛋的正下方,然后使用秘密武器便可以在瞬间收集到这个彩蛋。然而,彩蛋有一个魅力值,这个魅力值会随着彩蛋在空中降落的时间而降低,Sue要想得到更多的分数,必须尽量在魅力值高的时候收集这个彩蛋,而如果一个彩蛋掉入海中,它的魅力值将会变成一个负数,但这并不影响Sue的兴趣,因为每一个彩蛋都是不同的,Sue希望收集到所有的彩蛋。 然而Sandy就没有Sue那么浪漫了,Sandy希望得到尽可能多的分数,为了解决这个问题,他先将这个游戏抽象成了如下模型: 以Sue的初始位置所在水平面作为x轴。 一开始空中有N个彩蛋,对于第i个彩蛋,他的初始位置用整数坐标(xi, yi)表示,游戏开始后,它匀速沿y轴负方向下落,速度为vi单位距离/单位时间。Sue的初始位置为(x0, 0),Sue可以沿x轴的正方向或负方向移动,Sue的移动速度是1单位距离/单位时间,使用秘密武器得到一个彩蛋是瞬间的,得分为当前彩蛋的y坐标的千分之一。 现在,Sue和Sandy请你来帮忙,为了满足Sue和Sandy各自的目标,你决定在收集到所有彩蛋的基础上,得到的分数最高。

输入描述:

第一行为两个整数N, x0用一个空格分隔,表示彩蛋个数与Sue的初始位置。
第二行为N个整数xi,每两个数用一个空格分隔,第i个数表示第i个彩蛋的初始横坐标。
第三行为N个整数yi,每两个数用一个空格分隔,第i个数表示第i个彩蛋的初始纵坐标。
第四行为N个整数vi,每两个数用一个空格分隔,第i个数表示第i个彩蛋匀速沿y轴负方向下落的的速度。

输出描述:

一个实数,保留三位小数,为收集所有彩蛋的基础上,可以得到最高的分数。

示例1

输入
3 0
-4 -2 2
22 30 26
1 9 8
输出
0.000

数据范围:

N < = 1000,对于100%的数据。 -10^4 < = xi,yi,vi < = 10^4

思路

这类的区间DP,我们要消去时间的影响。
我们先对x进行排序。
f[L][R][0/1]:处理完[L, R]的球停在L/R的最大贡献。
在转移的时候,不在这个区间的球等待的贡献都加在这个区间。
例如f[L+1][R][0]->f[L][R][0],那么在转移时[1, L]和[R+1, n]的球都在等待
(他们的v和)*(等待时间)是这段时间失去的贡献。这样就消去了时间的影响

#include <bits/stdc++.h>
#define LL long long
using namespace std;

struct Node{
    LL x, y, v;
}a[1005];

LL f[1005][1005][2], s[1005];

int main() {

    memset(f, -0x3f, sizeof(f));
    int n, x0; scanf("%d%d", &n, &x0);
    for(int i=1; i<=n; i++){
        scanf("%lld", &a[i].x);
    }
    for(int i=1; i<=n; i++){
        scanf("%lld", &a[i].y);
    }
    for(int i=1; i<=n; i++){
        scanf("%lld", &a[i].v);
    }
    a[++n]={x0, f[0][0][0], 0};
    sort(a+1, a+n+1, [](Node &a, Node &b){return a.x<b.x;});

    for(int i=1; i<=n; i++) s[i]=s[i-1]+a[i].v;
    int pos=0;
    for(int i=1; i<=n; i++){
        if(a[i].x==x0&&a[i].y==f[0][0][0]){
            pos=i, f[i][i][0]=f[i][i][1]=0; break;
        }
    }
    for(int L=pos; L>=1; L--){
        for(int R=pos; R<=n; R++){
            if(L==R) continue;
            f[L][R][0]=max(f[L][R][0], max(f[L+1][R][0]-(a[L+1].x-a[L].x)*(s[L]+s[n]-s[R]), f[L+1][R][1]-(a[R].x-a[L].x)*(s[L]+s[n]-s[R]))+a[L].y);
            f[L][R][1]=max(f[L][R][1], max(f[L][R-1][0]-(a[R].x-a[L].x)*(s[L-1]+s[n]-s[R-1]), f[L][R-1][1]-(a[R].x-a[R-1].x)*(s[L-1]+s[n]-s[R-1]))+a[R].y);
            //cout<<L<<" "<<R<<" "<<f[L][R][0]<<" "<<f[L][R][1]<<endl;
        }
    }
    double ans=max(f[1][n][0], f[1][n][1])*0.001;
    printf("%.3f\n", ans);

    return 0;
}
posted @ 2020-12-09 22:47  liweihang  阅读(123)  评论(0编辑  收藏  举报
Live2D