简单基础动态规划(LIS+LCS+区间DP+树形DP)


LIS:最长上升子序列

Prince and Princess

UVA10635 Prince and Princess

本题可以将 $LCS$ (最长公共子序列)问题转换成 $LIS$ 问题。

我们可以根据第一个字符串进行新的编号,由先后顺序更新第二个字符串的编号,保证了单调的性,所以直接在第二个字符串中找最长不下降子序列即可。

最长上升子序列

再找最长上升子序列时,只用找到比我们存的数小的,就更新。可能不符合真正的最长上升序列,甚至不合法。但是它合理的原因是,他为的是保存最长上升的长度,如果有更长的子序列合法,那么就可以更新它的长度。

要想更新它的长度就要比现有的数都大,那么这个数一定会出现在合理的最长上升子序列中,所以这么存的长度合法。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=2e5+10;
int t,n,p,q,a[o_o],b[o_o],c[o_o];
int n_m[o_o],x_p,s_m;
void n_w(){
  memset(n_m,0,sizeof(n_m));
  x_p=s_m=0;
  for(int i=1;i<=p;i++)n_m[a[i]]=++x_p;//第一个数列编新编号 
  for(int i=1;i<=q;i++)//第二个序列更新所有编号 
    if(n_m[b[i]])c[++s_m]=n_m[b[i]];//存到新数组中 
}
int a_s,z_k[o_o];
void g_a(){
  a_s=1;//初始化答案(序列第一位相同) 
  z_k[1]=c[1];//最长上升子序列第一位初始化 
  for(int i=2;i<=s_m;i++){ 
    if(c[i]>z_k[a_s])z_k[++a_s]=c[i];//可以保持上升,存入 
    else {//不能保持上升 
      int p_s=lower_bound(z_k+1,z_k+1+a_s,c[i])-z_k;//找到第一个比他小的 
      z_k[p_s]=c[i];//更新数组 
    }
  }
}
int main(){
  scanf("%d",&t);
  for(int k=1;k<=t;k++){
    n=r_r(),p=r_r(),q=r_r();
    p++,q++;
    for(int i=1;i<=p;i++)a[i]=r_r();//读入第一个数组 
    for(int i=1;i<=q;i++)b[i]=r_r();//读入第二个数组 
    n_w();//赋新编号 
    g_a();//找结果 
    printf("Case %d: %d\n",k,a_s);//输出 
  }
  return 0;
}

LCS:最长公共子序列

History Grading

UVA111 History Grading

枚举匹配位置,记录匹配结果,继承状态。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
int n,t,d_p[31][31],n_m[31],t_p[31]; 
int main(){
  n=r_r();
  for(int i=0;i<n;i++){
    int x_x=r_r();
    n_m[x_x-1]=i;//按顺序标号正确答案 
  }
  while(cin>>t){
    t_p[t-1]=0;
    for(int i=1;i<n;i++){//按顺序标号学生答案 
      int x_x=r_r();
      t_p[x_x-1]=i;
    }
    memset(d_p,0,sizeof d_p);//多测清空 
    for(int i=0;i<n;i++){//枚举学生答案位置 
      for(int j=0;j<n;j++){//枚举答案位置 
        if(n_m[j]==t_p[i])d_p[i+1][j+1]=d_p[i][j]+1;//匹配成功,答案更新 
        else d_p[i+1][j+1]=max(d_p[i][j+1],d_p[i+1][j]);//那个匹配成功的多继承那个状态 
      }
    }
    printf("%d\n",d_p[n][n]);//输出所有答案与所有学生答案位置都匹配过的答案 
  }
  return 0;
}

区间DP

石子合并

P1880 NOI1995 石子合并

先枚举块长(从短到长),再枚举所有区块,枚举断点,计算每个区块的断点左半部分和右半部分,记上每次合并后的价值。所有元素都可能作为开头,所以都要找一遍,记录答案。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=201;
const int a_x=0x3f3f3f3f;
int n,m_i,m_a,f_1[o_o][o_o],f_2[o_o][o_o],n_m[o_o];  
int s[o_o];
int main(){   
    n=r_r();  
    for(int i=1;i<=n;i++){
        n_m[i]=r_r();//读入原序列 
        n_m[i+n]=n_m[i];//双倍长度方便计算以每个元素开头的情况 
    }
    for(int i=1;i<=n*2;i++)s[i]=s[i-1]+n_m[i];//计算前缀和 
    for(int p=1;p<n;p++){//枚举区块长 
        for(int i=1,j=i+p;(j<n*2)&&(i<n*2);i++,j=i+p){//在序列中枚举区块 
            f_2[i][j]=a_x;//初始赋最大值,找最小值 
            for(int k=i;k<j;k++){//枚举断点,从左右两部分找最优解继承 
                f_1[i][j]=max(f_1[i][j],f_1[i][k]+f_1[k+1][j]+s[j]-s[i-1]);//找最大值,通过前缀和快速计算区间和(合并石子得到的价值)  
                f_2[i][j]=min(f_2[i][j],f_2[i][k]+f_2[k+1][j]+s[j]-s[i-1]);//找最小值 
            }  
        }  
    }  
    m_i=a_x;//初始赋最大值,找最小值 
    for(int i=1;i<=n;i++){//遍历每个元素开头的情况,找到真正的最优解 
        m_a=max(m_a,f_1[i][i+n-1]);
        m_i=min(m_i,f_2[i][i+n-1]);  
    }  
    printf("%d\n%d",m_i,m_a);  
    return 0;  
}

能量项链

P1063 NOIP2006 提高组 能量项链

分开看,每个珠子就是一颗珠子,而不去考虑前后问题,再遍历区间的时候处理即可。

只要枚举区间,枚举断点,根据题目描述更新最大值即可。题目计算能量的方法,正好是枚举左端点(前一个珠子的头标记),断点 $+1$ (前一个珠子的尾标记,后一个珠子的头标记),右端点 $+1$ (后一个珠子的尾标记)的能量积。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=201; 
int n;
int a_a[o_o];
int d_p[o_o][o_o];
int m_a=-1;
int main(){
  n=r_r();
  for(int i=1;i<=n;i++)a_a[i]=r_r(),a_a[i+n]=a_a[i];//存两边,方便枚举每个开头的情况 
  for(int i=2;i<2*n;i++){//枚举结尾 
    for(int j=i-1;i-j<n&&j>=1;j--){//在长度范围内,枚举左区间 
      for(int k=j;k<i;k++)//枚举断点 
        //左边最大值 + 右边最大值 + 加持能量(题目中描述) 更新最大值 
        d_p[j][i]=max(d_p[j][i],d_p[j][k]+d_p[k+1][i]+a_a[j]*a_a[k+1]*a_a[i+1]);
      if(d_p[j][i]>m_a)m_a=d_p[j][i];//更新答案 
    }
  }
  cout<<m_a<<endl;
  return 0;
}

Coloring Brackets

CF149D Coloring Brackets

初始化所有新遍历到的括号,从大区间向小遍历,最后回溯时,等新答案,做后统计答案输出即可。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int m_a=1001;
const int m_d=1e9+7;
int n,g_y[m_a];//存配对括号互存下标 
long long d_p[m_a][m_a][3][3];//存情况 
char s[m_a];//读入括号序列 
stack<int>s_t;//用于匹配括号 
void d_f(int l,int r){
  if(l+1==r)//是相邻的一对括号,初始化 
    d_p[l][r][0][1]=d_p[l][r][0][2]=d_p[l][r][1][0]=d_p[l][r][2][0]=1;
  else if(g_y[l]==r){//现在左右是一对括号 
    d_f(l+1,r-1);//更新左右端点 
    for(int i=0;i<=2;i++)//枚举状态可能,更新答案 
      for(int j=0;j<=2;j++){
            if(j!=1)//现在右括号和匹配左括号不同色,统计答案 
          d_p[l][r][0][1]=(d_p[l][r][0][1]+d_p[l+1][r-1][i][j])%m_d;
            if(j!=2)//现在右括号和匹配左括号不同侧,统计答案 
          d_p[l][r][0][2]=(d_p[l][r][0][2]+d_p[l+1][r-1][i][j])%m_d;
            if(i!=1)//现在左括号和匹配右括号不同色,统计答案 
          d_p[l][r][1][0]=(d_p[l][r][1][0]+d_p[l+1][r-1][i][j])%m_d;
            if(i!=2)//现在左括号和匹配右括号不同色,统计答案 
          d_p[l][r][2][0]=(d_p[l][r][2][0]+d_p[l+1][r-1][i][j])%m_d;
      }
  }
  else{
    d_f(l,g_y[l]);//更新左括号匹配的范围 
    d_f(g_y[l]+1,r);//匹配左括号匹配的右括号外的下标,继续匹配。 
    for(int i=0;i<=2;i++)//枚举第一对括号的左括号 
      for(int j=0;j<=2;j++)//枚举第一对括号的右括号 
        for(int p=0;p<=2;p++)//枚举第二对括号的左括号 
          for(int q=0;q<=2;q++){//枚举第二对括号的右括号 
            if((j==1&&p==1)||(j==2&&p==2)) continue;//匹配相邻同色跳过 
            d_p[l][r][i][q]=(d_p[l][r][i][q]+d_p[l][g_y[l]][i][j]*d_p[g_y[l]+1][r][p][q]%m_d)%m_d;//合法更新答案 
          }
  }
}
int main(){
  scanf("%s",s);
  n=strlen(s);//读取原串 
  for(int i=0;i<n;i++){//匹配括号,互存另一半括号下标 
    if(s[i]=='(')s_t.push(i);
    else g_y[s_t.top()]=i,g_y[i]=s_t.top(),s_t.pop();
  }
  d_f(0,n-1);//从边界开始查询方案数 
  long long a_s=0;
  
  //由于括号染两种颜色的没有存,所以是 0,不影响统计结果 
  for(int i=0;i<=2;i++)//枚举左括号染色情况 
    for(int j=0;j<=2;j++)//枚举右括号染色情况 
      a_s=(a_s+d_p[0][n-1][i][j])%m_d;//统计所有情况 
      
  printf("%lld\n",a_s);//输出结果 
  return 0;
}

压缩

P2470 SCOI2007 压缩

区间枚举块长,左端点,找到压缩后的最短长度更新。难点在于区间内有 $M$ 的压缩,要考虑新压缩后的长度与原压缩的那个短,不断取最优。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=101;
using namespace std;
char s_s[o_o];
int d_p[o_o][o_o][3];
int n;
int f_i(int l,int r){
  if((r-l+1)&1)return 0;//长度是奇数,不可能压缩 
  int m_i=(l+r)>>1;//找到查抄串的一半处 
  for(int i=l;i<=m_i;i++)//前后匹配 
    if(s_s[i]!=s_s[i+m_i-l+1])return 0;//前后不同,不能压缩 
  return 1;//压缩成功 
}
int main(){
  scanf("%s",s_s+1);//读取字符串 
  n=strlen(s_s+1);//获取字符串长度 
  memset(d_p,0x3f,sizeof(d_p));//初始化 
  for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++)
      d_p[i][j][0]=d_p[i][j][1]=(j-i+1);//初始化区间长度 
  for(int l_n=2;l_n<=n;l_n++)//枚举长度 
    for(int l=1;l+l_n-1<=n;l++){//枚举左端点
      int r=l+l_n-1;//右端点 
      if(f_i(l,r))d_p[l][r][0]=min(d_p[l][(l+r)>>1][0]+1,d_p[l][r][0]);//查找成功,更新比对压缩户后的长度 
      //枚举断点,更新答案 
      for(int k=l;k<r;k++)//区间内没有 M(压缩开头) 
        d_p[l][##r][0]=min(d_p[l][r][0],d_p[l][k][0]+r-k);//查找区间内压缩后的最小值 
      for(int k=l;k<r;k++)//区间内有 M(压缩开头) 
        //更新端点左边有 M 和没有 M 的最小值,和右边有 M 和没有 M 的最小值的总的最小值(抽象:选择 M 的放置地点) 
        d_p[l][r][1]=min(d_p[l][r][1],min(d_p[l][k][0],d_p[l][k][1])+min(d_p[k+1][r][0],d_p[k+1][r][1])+1);
    }
  printf("%d\n",min(d_p[1][n][1],d_p[1][n][0]));
  return 0;
}

树形DP

“访问”美术馆

P1270 “访问”美术馆

枚举将时间分给两个儿子(向左,向右走),暴力统计最大值即可。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=1001;
int d_p[o_o][o_o];
struct po{
    int l_n,v;//道路花费时间,画的数量 
}p_p[o_o<<4];
int s_m;
int l_l(int k){//左儿子 
  return k<<1;
}
int r_r(int k){//右儿子 
  return k<<1|1;
}
void yv(int x){
    p_p[x].l_n=r_r(),p_p[x].v=r_r();
    p_p[x].l_n<<=1;//一去一回,两倍 
    if(!p_p[x].v){//若为零说明是走廊(见题目) 
        yv(l_l(x));//左儿子 
    yv(r_r(x));//右儿子 
    }
}
int d_f(int x,int s_m){
    if(d_p[x][s_m]>0||!s_m)return d_p[x][s_m];//已经遍历过或时间已经耗尽返回 
    if(p_p[x].v){//是展室 
      d_p[x][s_m]=min(p_p[x].v,(s_m-p_p[x].l_n)/5);//剩下的时间是全拿,还是来这里之后拿能拿部分(来这里要花费时间) 
      return d_p[x][s_m];
  }
    for(int k=0;k<=s_m-p_p[x].l_n;k++)//枚举剩下时间,分给两儿子 
        d_p[x][s_m]=max(d_p[x][s_m],d_f(l_l(x),k)+d_f(r_r(x),s_m-p_p[x].l_n-k));//取最大能拿到的画 
    return d_p[x][s_m];
}
int main(){
    s_m=r_r()-1;//要在警察到来前跑走(-1) 
    yv(1);//读入 
    printf("%d",d_f(1,s_m));//从第一条走廊开始找 
    return 0;
}

Book of Evil

CF337D Book of Evil

先找到里根节点最远的点,在以这个点为跟,重新跑一遍,更新新深度。再找最深的节点,以这个节点为根节点,在新数组中重新跑一遍深度。这样,可以知道每个点距离两个最远互相怪物的点的距离,若最远的点符合要求,那么就合法。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<stack>
using namespace std;
long long r_r(){//快读 
  long long x=0,f=1;
  char c=getchar();
  while(!isdigit(c)){
    if(c=='-')f=-1;
    c=getchar();
  }
  while(isdigit(c)){
    x=(x<<1)+(x<<3)+(c^48);
    c=getchar();
  }
  return x*f;
}
const int o_o=1e5+10;
struct po{
  int v,n_t;
}p_p[o_o<<1];
int n,m,d,s_m,t_p,a_s;
int v_l[o_o],d_p_1[o_o],d_p_2[o_o],h_d[o_o];
void a_d(int u,int v){
  p_p[++s_m].n_t=h_d[u];
  p_p[s_m].v=v;
  h_d[u]=s_m;
}
void d_1(int u,int f_a,int d_p){
  d_p_1[u]=d_p;//存深度 
  for(int i=h_d[u];i;i=p_p[i].n_t){
    int v=p_p[i].v;//下一个点 
    if(v!=f_a)d_1(v,u,d_p+1);//不返回走过的点 
  }
}
void d_2(int u,int f_a,int d_p){//找深度 
  d_p_2[u]=d_p;//存深度 
  for(int i=h_d[u];i;i=p_p[i].n_t){
    int v=p_p[i].v;//下一个点 
    if(v!=f_a)d_2(v,u,d_p+1);//不返回走过的点 
  }
}
int main(){
  n=r_r(),m=r_r(),d=r_r();
  for(int i=1;i<=m;i++)v_l[i]=r_r();//存初始怪物生成地点 
  for(int i=1;i<n;i++){//连边 
    int u=r_r(),v=r_r();
    a_d(u,v);
    a_d(v,u);
  }
  d_1(1,0,0);//从一节点开始找 
  for(int i=1;i<=m;i++)//找到怪物中深度最大的点 
    if(d_p_1[v_l[i]]>d_p_1[t_p])t_p=v_l[i];
  d_1(t_p,0,0);//从怪物最大深度的点开始找,重新赋值深度 
  t_p=0;//清零 
  for(int i=1;i<=m;i++)//找到怪物中深度最大的点 
    if(d_p_1[v_l[i]]>d_p_1[t_p])t_p=v_l[i];
    d_2(t_p,0,0);//继续找点离这个点的深度 
  for(int i=1;i<=n;i++)//从第一个点开始找,若距离都符合,计数 
    if(d_p_1[i]<=d&&d_p_2[i]<=d)a_s++;
  printf("%d\n",a_s);
  return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录