区间和序列上的dp

懵懂的女人 提交于 2019-12-03 13:06:05

区间上的dp状态设计最基本的形式:

\(F[i]\)表示以i结尾的最优值或方案数。
\(F[i][k]\)表示以i结尾附加信息为k的最优值或方案数。
当然可以有多维附加信息。
转移的话往往是枚举上一个断点。
\(F[i]=max \{ F[j]+ w(j+1,i) | j是一个满足转移条件的断点\}\)
另一个很常见的是:$ f[i][j]$前i个位置分成j段/选出j个的最优值。
这是最简单的一类序列上的dp

bzoj1003

有m个码头和e条航线,每天航线有成本。有连续n天需要从1号码头到m号码头运输货物。每个码头会在某些天数区间内不许经过。每更换一次运输路线,要付出k的成本。
求这n天的最小总成本。
m<=20, n<=100
SOLUTION:
其实就是分成很多段,每一段选同一个运输路线,然后得到一个最优的划分方案,使得成本最小。
f[i]表示前i天的运输最小成本。
\(f[i]=min\{ f[j]+k+w(j+1,i)*(i-j) | j<i \}\)
其中w(x,y)表示最短的在第x天到第y天都能用的路线长度;

处理方法:

首先枚举所有的x,y,然后利用最短路算法(这里dijkstra)算出x~y这些天都可以满足的1到m的最短路径;(计算方法:首先数组$use[i][j] $记录第i个点在第j天是否可以使用,在dijkstra时传入x和y,提前预处理,用数组used[i]记录在第x天到第y天是否可以使用码头i,在更新时如果遇到不能使用的点,直接continue掉即可)

复杂度O(N^2 * m * log(m)) 然后数据范围很小嘛所以可以过√

CODE:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
#define pr pair<int,int>
#define mk make_pair

using namespace std;

inline int read(){
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}

const int inf=2147483647;
struct node{
    int to,dis,nxt;
}e[500];
int ecnt,head[25];
void add(int from,int to,int dis){
    ++ecnt;
    e[ecnt].to=to;
    e[ecnt].dis=dis;
    e[ecnt].nxt=head[from];
    head[from]=ecnt;
}
int n,m,k,E;
int d;
long long wei[110][110];
bool use[25][110];
void hs(int p,int f,int t){//记录第p个码头在第f天到第t天不能用
    for(int i=f;i<=t;i++)
        use[p][i]=1;
}
bool vis[25],used[25];
int dis[25];
priority_queue<pr,vector<pr>,greater<pr> > q;
long long dijkstra(int x,int y){
    while(!q.empty()) q.pop();
    memset(used,0,sizeof(used));
    memset(vis,0,sizeof(vis));
    q.push(mk(0,1));
    for(int i=1;i<=m;i++)
        dis[i]=inf;
    for(int i=x;i<=y;i++)
        for(int j=1;j<=m;j++)
            if(use[j][i]) used[j]=1;//预处理x~y天不能用的码头,标记为1;
    dis[1]=0;

    while(!q.empty()){
        int u=q.top().second;
        q.pop();
        if(vis[u]) continue;
        vis[u]=1;
        for(int i=head[u],v,w;i;i=e[i].nxt){
            v=e[i].to;w=e[i].dis;
            if(used[v]) continue;//遇到不能用的码头,continue;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                q.push(mk(dis[v],v));
            }
        }
    }
    return dis[m];
}

long long f[110];

void dp(){//计算前n天从1=>m码头的最短花费
    //显然最短花费是
    for(int i=1;i<=n;i++){
        f[i]=(long long)wei[1][i]*i;
        for(int j=1;j<i;j++){
            f[i]=min(f[i],f[j]+k+wei[j+1][i]*(i-j));
        }
    }
}

int main(){
    n=read();m=read();
    k=read();E=read();
    for(int i=1,u,v,w;i<=E;i++){
        u=read();
        v=read();
        w=read();
        add(u,v,w);
        add(v,u,w);
    }
    d=read();
    for(int i=1,p,a,b;i<=d;i++){
        p=read();
        a=read();
        b=read();
        hs(p,a,b);
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            wei[i][j]=dijkstra(i,j);
    dp();
    cout<<f[n]<<endl;
    return 0;
}

bzoj1296 粉刷匠

有n条木板要被粉刷,每条木板分为m个格子,每个格子需要被刷成蓝色或红色。

每次粉刷可以在一条木板上给连续的一段格子刷上相同的颜色。每个格子最多被刷一次。

问若只能刷k次,最多正确粉刷多少格子。

n,m<=50, k<=2500

如果只有一条木板,那么设\(g[i][j]\)表示前i个格子刷j次的最多正确格子

然后枚举一个k,表示前k个格子刷了j-1次,第k+1到第i个格子刷一次的情况,这样依次枚举求出\(g[i][j]\)的最大值

\[ g[i][j]=max\{ g[k][j-1]+w(k+1,i) | k<i \}\]

w(x,y)为第x到第y个格子的最多同色格子数,哪个颜色出现的多刷哪个,直接记一个前缀和即可。???对怎么处理w(x,y)表示疑惑???

大概是这个意思叭:

for(int i=1;i<=m;i++){
    if(color[i]=='0') {
        Sumr[i]=Sumr[i-1]+1;
        Sumb[i]=Sumb[i-1];
    }
    else {
        Sumb[i]=Sumb[i-1]+1;
        Sumr[i]=Sumr[i-1];
    }
}
w[i][j]=max(Sumb[j]-Sumb[i-1],Sumr[j]-Sumr[i-1]);

有多条木板,设\(f[i][j]\)表示前i个木板刷j次的最大答案。

\(f[i][j]=Max\{ f[i-1][k]+g_i[m][j-k] | k<=j \}\)

也就是需要先处理出每一块木板的g数组;


然后叭,其实这是一个有TLE的代码,但\(o_2\)优化,你值得拥有!

基本上都是上面↑讲到的式子叭,但是有几个要注意的点:

  1. 处理g数组时,枚举k要从0开始枚举;
  2. \(g[i][j][k]\)在k!=0时,初值为1;k=0时初值为0;
#include<bits/stdc++.h>

using namespace std;

inline int read(){
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}

int n,m,t;
int g[55][55][2505];
char color[60];
int Sumr[55],Sumb[55];
int f[55][2505];

void hs(int n){
    for(int i=1;i<=m;i++)
        for(int j=1;j<=t;j++){
            g[n][i][j]=1;
            for(int k=0;k<i;k++)
                g[n][i][j]=max(g[n][i][j],g[n][k][j-1]+max(Sumr[i]-Sumr[k],Sumb[i]-Sumb[k]));
        }
}

void clear(){
    for(int i=0;i<=m;i++) Sumr[i]=Sumb[i]=0;
}

int main(){
    n=read();m=read();t=read();
    for(int i=1;i<=n;i++) {
        scanf("%s",color+1);
        clear();
        for(int j=1;j<=m;j++){
            if(color[j]=='1') {
                Sumr[j]=Sumr[j-1];
                Sumb[j]=Sumb[j-1]+1;
            }
            else {
                Sumr[j]=Sumr[j-1]+1;
                Sumb[j]=Sumb[j-1];
            }
        }
        hs(i);
    }
    for(int i=1;i<=n;i++)
        for(int j=0;j<=t;j++)
            for(int k=0;k<=j;k++)
                f[i][j]=max(f[i][j],f[i-1][k]+g[i][m][j-k]);
                
    cout<<f[n][t]<<endl;
    return 0;
}

括号序列模型及解法

给定一个长度为n的仅包含左右括号和问号的字符串,将问号变成左括号或右括号使得该括号序列合法,求方案总数。
例如(())与()()都是合法的括号序列。
n<=3000。

然后反正没找到例题叭,所以没有办法验证对错了,只能先写写看;

然鹅并没有写对啊,太难过了

只能先复制一下zhhx的solution了(溜

\(dp[i][j]\)表示当前到第i个字符,现在还有j个左括号。

那么分三种情况考虑。

  1. 若第i+1个字符是左括号,则能转移到\(dp[i+1][j+1]\)
  2. 若第i+1个字符是右括号,则能转移到\(dp[i+1][j-1]\)
  3. 若第i+1个字符是问号,则能转移到\(dp[i+1][j-1]\)\(dp[i+1][j+1]\)

最终\(dp[n][0]\)就是方案总数啦。
时间复杂度为\(O(n^2)\)

首先感谢小蒟蒻皮皮鱼的友情代码(真的自己写枯了,果然还是太弱了

思维上确实不如神仙皮皮鱼

神仙皮皮鱼把这道题出成了.jpg

U86873 小Y的精灵国机房之旅

#include<bits/stdc++.h>

using namespace std;

inline int read() {
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}
const int mxn=10010;
const int mod=1000000007;
int n;
char c[mxn];
int f[3][10010];

int main(){
    n=read();
    scanf("%s",c+1);
    f[0][0]=1;
    for(int i=1;i<=n;i++) {
        for(int j=i;j>=0;j--) {
            f[i&1][j]=0;
            if(c[i]=='Y') {
                if(j>0)
                    f[i&1][j]=f[(i-1)&1][j-1]%mod;
            }
            if(c[i]=='H') {
                f[i&1][j]=f[(i-1)&1][j+1]%mod;
            }
            if(c[i]=='C') { 
                f[i&1][j]=f[(i-1)&1][j+1]%mod;
                if(j>0) 
                    f[i&1][j]=(f[i&1][j]+f[(i-1)&1][j-1])%mod;
            }
        }
    }
    printf("%d",f[n&1][0]%mod);
    return 0;
}

BZOJ3709

在一款电脑游戏中,你需要打败n只怪物(从1到n编号)。为了打败第i只怪物,你需要消耗d[i]点生命值,但怪物死后会掉落血药,使你恢复a[i]点生命值。任何时候你的生命值都不能降到0(或0以下)。

请问是否存在一种打怪顺序,使得你可以打完这n只怪物而不死掉。

N<=10^5

这个题是不是蜜汁眼熟?

没错!它就是lyd之前讲贪心讲到的那个题:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#define ll long long

using namespace std;

inline int read(){
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}

int n,x,y,upc,downc;
ll z;
struct node{
    int a,d,id;
}up[100005],down[100005];

bool cmp1(node i,node j){
    return i.d<j.d;
}

bool cmp2(node i,node j){
    return i.a>j.a;
}

int main(){
    n=read();scanf("%lld",&z);
    for(int i=1;i<=n;i++){
        x=read();y=read();
        if(x>y) down[++downc].a=y,down[downc].d=x,down[downc].id=i;
        else up[++upc].a=y,up[upc].d=x,up[upc].id=i;
    }
    sort(up+1,up+upc+1,cmp1);
    sort(down+1,down+downc+1,cmp2);
    for(int i=1;i<=upc;i++){
        z-=up[i].d;
        if(z<=0){
            printf("NIE");
                        return 0;
        }
        z+=up[i].a;
    }
    for(int i=1;i<=downc;i++){
        z-=down[i].d;
        if(z<=0) {
            printf("NIE");
                        return 0;
        }
        z+=down[i].a;
    }
    printf("TAK\n");
    for(int i=1;i<=upc;i++)printf("%d ",up[i].id);
    for(int i=1;i<=downc;i++) printf("%d ",down[i].id);
    return 0;
}

bzoj4922

给出一些括号序列,要求选择一些括号序列拼接成一个合法的括号序列,使得总长最大。

1<=n<=300,表示括号序列的个数

括号序列的长度len不超过300.

这是一道调了3天的题(我太难了)

首先对于已经配对的括号,我们不必再去考虑,因此可以先将其删掉:

举个例子:

))()(),显然对于子串"()()",我们可以不考虑它。

那么最后消成的,一定是这样的序列:))…)((…((,左端为右括号,右端为左括号

因此我们可以先将这一部分处理掉:

//用x来记录没有消掉的右括号的数量,y记录没有消掉的左括号的数量
/*结构体struct node{
    int l,r,len;
    //l表示这个序列的左端右括号数
    //r表示这个序列的右端左括号数
    //len表示这段序列的长度
}a[mx]*/
for(int i=1,len,x,y;i<=n;i++){
        scanf("%s",s+1);
        x=y=0;
        len=strlen(s+1);
        sum+=len;//记录整个字符串共有多长
        a[i].len=len;
        for(int i=1;i<=len;i++) {
            if(s[i]=='(') y++;
            else y?y--:x++;
        }
        a[i].l=x;
        a[i].r=y;
    }

然后回到了一个很熟悉的题目:

按照这道题的贪心思路,我们把左括号看成+1,右括号看成-1,首先肯定是要考虑左边右括号小于右边左括号的,因此将左括号数量>右括号数量的排在前面,右括号数量>左括号数量的排在后面;

然后对于每一部分,我们又应该怎么排列呢?

对于左括号数量>右括号数量的,我们按照右括号数量,右括号越少,排的位置越靠前。

对于左括号数量<右括号数量的,我们按照左括号数量,左括号越多,排的位置越靠前。

bool cmp(node x,node y){
    if((x.l<x.r)&&(y.l<y.r)) return x.l<y.l;//如果比较的两个字符串都是左括号数>右括号数,比较右括号数量,右括号少的在前;
    if((x.l<x.r)&&(y.l<y.r)) return x.l<x.r;//如果比较的两个字符串中有一个满足左括号数>右括号数,另一个不满足(两个都满足的情况会在上面return掉),那么我们看一下x是否满足右括号数<左括号数,如果满足,显然x在前y在后,反之。
    return x.r>y.r;//最后剩下的是左括号数<右括号数的,也就按照左括号数从大到小排序;
}

int main(){
    ……
    sort(a+1,a+n+1,cmp);
    ……
    return 0;
}

按照这样排序之后,我们进行dp:

\(f[i][j]\)表示前i个字符串,左右括号之和为j(左+1右-1)时的长度最长是多少w?

然后考虑转移:

对于\(f[i][j]\)如果不选第i个子串:\(f[i][j]<=f[i-1][j]\)

如果选择第i个子串:\(f[i][j]<=f[i-1][j-a[i].r+a[i].l]+a[i].len\)

因此,\(f[i][j]\)最终结果是在两者中取max:\(f[i][j]=max(f[i-1][j],f[i-1][j-a[i].r+a[i].l]+a[i].len);\)

然后注意考虑\(j-a[i].r+a[i].l>=0\)

\(CODE:\)

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

inline int read(){
    int ans=0;
    char last=' ',ch=getchar();
    while(ch>'9'||ch<'0') last=ch,ch=getchar();
    while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
    if(last=='-') ans=-ans;
    return ans;
}

int n,B,A,ans;
char s[310];
struct node{
    int l/*left )*/,r/*right (*/,len;
}a[310];

int f[310][90001];

bool cmp(node x,node y){
   if((x.l<x.r)&&(y.l<y.r)) return x.l<y.l;
   if((x.l<x.r)||(y.l<y.r)) return x.l<x.r;
   return x.r>y.r;
}

int main(){
    n=read();
    //x:right brackets
    //y:left brackets
    int sum=0;
    for(int i=1,len,x,y;i<=n;i++){
        scanf("%s",s+1);
        x=y=0;
        len=strlen(s+1);
        sum+=len;
        a[i].len=len;
        for(int i=1;i<=len;i++) {
            if(s[i]=='(') y++;
            else y?y--:x++;
        }
        a[i].l=x;
        a[i].r=y;
    }
    
    sort(a+1,a+n+1,cmp);
    memset(f,200,sizeof(f));
    f[0][0]=0;
    for(int i=1;i<=n;i++){
        for(int j=0;j<=sum;j++){
            f[i][j]=f[i-1][j];
            if(i==n&&a[i].r!=0&&j==0) continue;
            if(j+a[i].l-a[i].r>=0)
            f[i][j]=max(f[i][j],f[i-1][j+a[i].l-a[i].r]+a[i].len);
        }
    }
    printf("%d",f[n][0]);
    return 0;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!