DP基础问题若干(ACM/ICPC)

【TOJ1765】最长上升子序列

状态表示:dp[i]  以a[i]为数列的最后一个数时最长上升子序列的长度

状态转移方程:if(a[i] >a[k])dp[i] = max(dp[i],dp[k]+1); 1<=k<=i-1;

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define max(a,b) (a) > (b) ? (a) : (b)
const int MAXN = 1010;
int a[MAXN],dp[MAXN];
int main(){
   int N;
   while(scanf("%d",&N) != EOF){
       for(int i = 1; i <= N; ++i){
           scanf("%d",&a[i]);
           dp[i] = 1;
       }
       for(int i = 1; i <= N; ++i)
       for(int k = 1; k <= i - 1; ++k){
           if(a[i] > a[k]){
                dp[i] = max(dp[i],dp[k] + 1);
           }
       }
       int ans = 0;
       for(int i = 1; i <= N; ++i){
           ans = max(ans,dp[i]);
       }
       /****************************/
       //输出找到的最长上升子序列
       /*
       int anss = ans;
       for(int i = N; i >= 1; --i){
           if(dp[i] == anss){
                anss --;
                printf("%d ",a[i]);
           }
       }
       puts("");
       */
       /***************************/
       printf("%d\n",ans);
   }
   return 0;
}


LIS(最长上升子序列)是比较简单的一道经典DP,另外还有LCS(最长公共子序列),还有一个较难的,LCIS(最长上升公共子序列)

【POJ2127】Greatest CommonIncreasing Subsequence

解析:定义max[i][j]为A串匹配到i,B串匹配到j时的最长公共上升子序列的长度。

若A[i]!=B[j] 那么max[i][j]=MAX{max[i'][j']|i'<i,j'<j}

若A[i]==B[j] 那么max[i][j]=MAX{max[i'][j']+1|i'<i,j'<j,A[i']<A[i],B[j']<B[j]}

这一个O(n^4) 的算法,但是我们会发现max[i][j]长度会增加1的时候都是在A[i]==B[j]情况下,那么我们应该考虑如何应用这个性质呢。所以我们以A串为基准,重新定义max[i][j]:以A[i]为基准匹配串末尾时(A[i]必选),匹配到B[j]时的最长公共上升子序列的长度

若A[i]!=B[j] 那么max[i][j]=max[i][j-1]

若A[i]==B[j] 那么max[i][j]=MAX{max[i'][j-1]+1|i'<i,A[i']<A[i]}

通过这样的定义我们保证了,对于任何max[i][j],max[i][j]里面存放的是已当前A[i]为匹配的串的最后一个元素时的最大长度。我们可以看到这个方程的复杂度只有O(n^3) ,对于j的循环就没有必要了。

代码:

#include<iostream>
using namespace std;
int a[505],b[505];
int dp[505][505];
int n,m;
int path[505][505];
int I,J,ans,res[505];
int main()
{
   int i,j,k;
   while(scanf("%d",&n)!=-1)
   {
       ans=0;
       for(i=0;i<n;i++)
           scanf("%d",&a[i]);
       scanf("%d",&m);
       for(i=0;i<m;i++)
           scanf("%d",&b[i]);
       memset(path,-1,sizeof(path));
       memset(dp,0,sizeof(dp));
       for(i=1;i<=n;i++)
           for(j=1;j<=m;j++)
           {
                dp[i][j]=dp[i-1][j];
                if(a[i-1]==b[j-1])
                {
                    int max=0;
                    for(k=1;k<j;k++)
                       if(b[k-1]<b[j-1]&&dp[i][max]<dp[i][k])
                            max=k;
                   if(dp[i][max]+1>dp[i][j])
                    {
                        dp[i][j]=dp[i][max]+1;
                        path[i][j]=max;
                        if(dp[i][j]>ans)
                        {
                            ans=dp[i][j];
                            I=i;
                            J=j;
                        }
                    }
                }
           }
        printf("%d\n",ans);
       k=ans;
       while(ans)
       {
           if(path[I][J]>=0)
           {
                res[ans--]=b[J-1];
                J=path[I][J];
           }
           I--;
       }
       for(i=1;i<=k;i++)
           printf("%d ",res[i]);
       printf("\n");
   }
}


【NKU1137】石子合并问题

题目大意:在一个圆形操场的四周摆放着n 堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2 堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分。

解析:如果这些石子一条直线排列,那么可以将1到n的合并拆分为两个结构相同的1-k的一堆和k+1到n的一堆石子合并的问题,就可以用dp[i][j]表示合并i到j的石子所能得到的最优值。但本题的石子围城了一个圈,就必须改变dp数组的设计

状态表示:dp[i][j]表示从第i块石头开始数起,合并j块石头所能得到的最优值

状态转移方程:dp[i][j] =better(dp[i][j],dp[i][i+k]+dp[(i+k-1)%n+1][j-k]+sum[i][j]);(1<=k<=j-1)

初始态(DP边界):dp[i][1] = 0;

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define INF 0x1f1f1f
#define max(a,b) (a) > (b) ? (a) : (b)
#define min(a,b) (a) < (b) ? (a) : (b)
 
const int MAXN = 110;
int sum[MAXN][MAXN];  //sum[i][j]表示从i开始数j个石头重量的和
int dp[MAXN][MAXN];   //dp[i][j]表示从i开始数j个石头合并以后的最大得分
int dp1[MAXN][MAXN];
int n,val[MAXN];
 
int main(){
   while(scanf("%d",&n) != EOF){
       for(int i = 1; i <= n; ++i)scanf("%d",&val[i]);
       for (int i = 1; i <= n; ++i)
           sum[i][1] = val[i];
       for (int j = 2; j <= n; ++j)
           for (int i = 1; i <= n; ++i)
                sum[i][j] = val[i] +sum[(i+1-1)%n+1][j - 1];
       for(int i = 0; i <= n; ++i)dp[i][1] = dp1[i][1] = 0;
 
       for(int j = 2; j <= n; ++j){ //这里必须把j放到外层以保证用到的时候已经求出
           for(int i = 1; i <= n; ++i){
 
                dp[i][j] = 0;
                dp1[i][j] = INF;
                for(int k = 1; k < j; ++k){
                    dp1[i][j] =min(dp1[i][j],dp1[i][k] + dp1[(i+k-1)%n+1][j-k]+sum[i][j]);
                    dp[i][j] =max(dp[i][j],dp[i][k] + dp[(i+k-1)%n+1][j-k]+sum[i][j]);
                    //此处用到的是dp[][j-k],所以要保证dp[1..n][j-k]已经求出,所以j放循环外层
                }
           }
       }
 
       int ans = 0,ans1 = INF;
       for(int i = 1; i <= n; ++i){
           ans = max(ans,dp[i][n]);
           ans1 = min(ans1,dp1[i][n]);
       }
       printf("%d\n",ans1);
       printf("%d\n",ans);
   }
   return 0;
}


【POJ1160】POST OFFICE 邮局问题

【题目大意】:用数轴描述一条高速公路,有V个村庄,每一个村庄坐落在数轴的某个点上,需要选择P个村庄在其中建立邮局,要求每个村庄到最近邮局的距离和最小。

【题目分析】:经典DP

1、考虑在V个村庄中只建立【一个】邮局的情况,显然可以知道,将邮局建立在中间的那个村庄即可。也就是在a到b间建立一个邮局,若使消耗最小,则应该将邮局建立在(a+b)/2这个村庄上(可以通过画图知道)。

2、下面考虑建立【多个】邮局的问题,可以这样将该问题拆分为若干子问题,在前i个村庄中建立j个邮局的最短距离,是在前【k】个村庄中建立【j-1】个邮局的最短距离 与 在【k+1】到第i个邮局建立【一个】邮局的最短距离的和。而建立一个邮局我们在上面已经求出。

3、状态表示,由上面的讨论,可以开两个数组

dp[i][j]:在前i个村庄中建立j个邮局的最小耗费

sum[i][j]:在第i个村庄到第j个村庄中建立1个邮局的最小耗费

那么就有转移方程:dp[i][j] =min(dp[i][j],dp[k][j-1]+sum[k+1][i])  DP的边界状态即为dp[i][1] = sum[1][i]; 所要求的结果即为dp[vil_num][post_num];

4、然后就说说求sum数组的优化问题,可以假定有6个村庄,村庄的坐标已知分别为p1,p2,p3,p4,p5,p6;那么,如果要求sum[1][4]的话邮局需要建立在2或者3处,放在2处的消耗为p4-p2+p3-p2+p2-p1=p4-p2+p3-p1 放在3处的结果为p4-p3+p3-p2+p3-p1=p4+p3-p2-p1,可见,将邮局建在2处或3处是一样的。现在接着求sum[1][5],现在处于中点的村庄是3,那么1-4到3的距离和刚才已经求出了,即为sum[1][4],所以只需再加上5到3的距离即可。同样,求sum[1][6]的时候也可以用sum[1][5]加上6到中点的距离。所以有递推关系:sum[i][j] = sum[i][j-1] + p[j] -p[(i+j)/2]

代码:

#include <cstdio>
#include <cstring>
using namespace std;
#define min(a,b) (a) < (b) ? (a) : (b)
int dp[310][31];
int sum[310][310];
int V,P;
int pos[310];
int main(){
   while(scanf("%d%d",&V,&P) != EOF){  //少写了一个%d直接造成结果不对,晕死
       for(int i = 1; i <= V; ++i)scanf("%d",&pos[i]);
       memset(sum,0,sizeof(sum));
       for(int i=1 ; i< V ; i++){
       for(int j=i+1 ; j<= V ; j++){
           sum[i][j] = sum[i][j-1]+ pos[j]  -  pos[(i+j) / 2];
       }
       }
       for(int i = 1; i <= V; ++i){
           dp[i][i] = 0;
           dp[i][1] = sum[1][i];
       }
       for(int j = 2; j <= P; ++j){ //注意为什么把它放在外层
            for(int i = j+1; i <= V; ++i){
 
                dp[i][j] = 9999999;
                for(int k = j-1; k < i; ++k)
                    dp[i][j] =min(dp[i][j],dp[k][j-1]+sum[k+1][i]);
           }
       }
 
       printf("%d\n",dp[V][P]);
   }
   return 0;
}


  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值