dp部分一:线性动态规划算法
知识概述
动态规划算法(简称dp)是算法设计中的重要组成部分,往往在求解全局最优或统计数量的时候使用。动态规划算法的学习历程就可以与贪心算法学习类比,不同的是贪心算法的求解目的是局部最优解。即在进行算法求解时只能保证求出的解在已知的约束条件下是成立的,某些复杂问题使用贪心算法可能会求出多个不同的局部最优解,难以判断哪个才是全局最优解。动态规划问题的求解则是递推的求出全局最优解,利用全局最优解,一定包含全局最优子解这种思路。对要求解的问题进行多步的简化和分解,最终落实到一个递推式的生成上。
动态规划的算法实质理解清楚之后,可以发现这种算法并不是具体的明文算法,而是类似贪心、分治的思维算法。利用最优解包含最优子解的原理,逻辑推理出:在求一个最优解时,只有过去求得的解会影响该解,未来求得的解不会。利用这个推论,可以将新解的求得表示为多个已知解的递推式。整个求解过程中,由于保持了每一步都是基于已知量采取的最优策略,可以保证每个解的求得都是最优的,进而求得了全局最优解。
综上,可以发现在动态规划问题中,两个问题的求解至关重要:初始状态和状态转移递推关系式。解决一个动态规划的求解问题时,寻找到准确的初态和有效的状态转移关系,是十分高效的通道。
看完这些,可能你对动态规划已经有所了解,下面用三道经典的动态规划问题来举例说明动态规划的求解。
题目一
题目概述
东东有两个序列A和B。
他想要知道序列A的LIS和序列AB的LCS的长度。
注意,LIS为严格递增的,即a1<a2<…<ak(ai<=1,000,000,000)。
INPUT&输入样例
第一行两个数n,m(1<=n<=5,000,1<=m<=5,000)
第二行n个数,表示序列A
第三行m个数,表示序列B
输入样例:
5 5
1 3 2 5 4
2 4 3 1 5
OUTPUT&输出样例
输出一行数据ans1和ans2,分别代表序列A的LIS和序列AB的LCS的长度
输出样例:
3 2
题目重述
题目综合了两个线性动态规划的基本问题:最长上升子序列(LIS)和最长公共子序列(LCS)。对A序列求解LIS,对AB求解器最长的公共子序列。
最长上升子序列定义:在一个序列中,依次选取出一个子序列,且保证选出的子序列严格有序(a1<a2<a3<…<ak),则称为该序列为上升子序列。最长上升子序列就是所有序列中,选取的子序列长度最大的。
最长公共子序列定义:在两个序列中,如果有部分子序列数值和顺序是完全一致的,则称为公共子序列。最长公共子序列就是两个序列中最长的公共序列。
思路概述
LIS和LCS是线性动态规划的经典问题,在学习dp的过程中是不可不做的例题。
LIS求解
LIS问题求解思路:
LIS的序列求解要求有两个:1、保持序列的顺序 2、数值大小的顺序
为了保证这两个要求都能够满足,将要求的dp解定义为:
dp[i]—>以Ai为结尾的最长子序列的长度
经过思考,我们可以写出如下的状态转移表达式
dp[1]=1
dp[i]=max{dp[j] | j<i && Aj<Ai}+1
简单的递推求解即可求出全部最优解。
LCS 求解
LCS问题求解思路:
LCS的核心要求是求出相对于两个序列的最长公共子序列
为了满足两个序列的要求,使用二维dp解来存储序列长度:
dp[i][j]—>到A序列的i位置B序列的j位置最长的公共子序列
状态转移转换考虑两种情况即可:
A和B序列 i 和 j 位置值相等—>dp[i][j]=dp[i-1][j-1]+1
A和B序列 i 和 j 位置值不等—>dp[i][j]=max(dp[i][j-1],dp[i-1][j])
同样只需要简单的递推即可求出全局最优解。
源码(C++)
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int M=1e4;
int alist[M];
int blist[M];
int dplis[M];
int dplcs[M][M];
int lis(int m)
{
dplis[1]=1;
int max_res=1;
for(int i=2;i<=m;i++)
{
int max_value=0;
for(int j=1;j<i;j++)
{
if(alist[j]<alist[i])
{
max_value=max(max_value,dplis[j]);
}
}
dplis[i]=max_value+1;
max_res=max(max_res,dplis[i]);
}
return max_res;
}
int lcs(int m,int n)
{
dplcs[0][0]=0;
dplcs[0][1]=0;
dplcs[1][0]=0;
int max_res=0;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
if(alist[i]==blist[j])
dplcs[i][j]=dplcs[i-1][j-1]+1;
else
dplcs[i][j]=max(dplcs[i-1][j],dplcs[i][j-1]);
max_res=max(max_res,dplcs[i][j]);
}
return max_res;
}
int main()
{
int m,n;
scanf("%d %d",&m,&n);
for(int i=1;i<=m;i++)
scanf("%d",&alist[i]);
for(int i=1;i<=n;i++)
scanf("%d",&blist[i]);
int res1=lis(m);
printf("%d ",res1);
int res2=lcs(m,n);
printf("%d\n",res2);
return 0;
}
题目二
题目概述
给一个序列,里边有 n 个数,每一步能拿走一个数,比如拿第 i 个数, Ai = x,得到相应的分数 x,但拿掉这个 Ai 后,x+1 和 x-1 (如果有 Aj = x+1 或 Aj = x-1 存在) 就会变得不可拿(但是有 Aj = x 的话可以继续拿这个 x)。求最大分数。
INPUT&输入样例
第一行包含一个整数 n (1 ≤ n ≤ 105),表示数字里的元素的个数
第二行包含n个整数a1, a2, …, an (1 ≤ ai ≤ 105)
输入样例:
9
1 2 1 3 2 2 2 2 3
OUTPUT&输出样例
输出一个整数:n你能得到最大分值。
输出样例:
10
题目重述
给出n个数字序列,是无序且有重复的。现规定一个取数的策略:取到x之后可以多次取x,但是他的相邻数字x-1和x+1不可以取,写出针对所给的数字序列能够取到的数字之和。
输入输出样例的说明:
取2之后,1和3不可以取到,取5次2,结果为10.
思路概述
初步分析这道题的特点,可能会认为在求解使需要同时知晓已知解和未来要求出的(未知解),貌似并不满足使用动态规划问题的前提特征。但如果列举一下解的特征就会发现,某个数字是否被选取可以用x+1和x-1的形式表示,也可以用x-2和x-1的形式表示。只要我们能够较好的设计动态规划状态数组和状态转移方程就可以简单的使用线性dp解决该问题。
将dp[i]数组设置为到数字i位置能选的最大和,每个数字在序列中出现的次数存放为a[i]。显而易见的是dp[0]=0 dp[1]=1*a[1]
当i>1时,状态转移方程可以描述为dp[i]=max(dp[i-1],dp[i-2]+i*a[i])。原理即是在求一个解产生抉择是否选取时,取值有两种可能:
1、从i-2到i-1没有进行更新,则i位置可以选数更新,即为dp[i-2]+a[i]*i
2、从i-2到i-1进行了选数更新,该位置不可以选数,则结果应该与i-1位置相同,即为dp[i-1]
由于题目要求选取最大和,每一步的求解都取两个抉择中结果较大的选项即可。
坑点
可能看完上面的思路,你觉得这个题思路清晰,十分容易就AC了。但是贸然提交,就会得到一个WA,是我们的思路出现了问题么?
答案:并不是。而是数据范围上挖了坑。虽然每个数字只有1e5,但是由于最多可能出现1e5个数字,且允许多个重复。一旦测试数据中出现1e5个1e5相加这种坑点数据,就会造成精度被爆而WA。注意特殊数据并改动为long long数据类型即可解决问题。
源码(C++)
#include<stdio.h>
#include<iostream>
#include<stdlib.h>
#include<string.h>
using namespace std;
const int M=1e6+10;
long long list[M];
long long dp[M];
long long max_selectnum(int max_m)
{
dp[0]=0;
dp[1]=list[1];
long long max_res=0;
for(int i=2;i<=max_m;i++)
{
long long val=list[i]*i;
dp[i]=max(dp[i-1],dp[i-2]+val);
max_res=max(max_res,dp[i]);
}
return max_res;
}
int main()
{
int number;
scanf("%d",&number);
for(int i=0;i<=M;i++)
list[i]=0;
int cin_value=0;
int max_value=0;
for(int i=0;i<number;i++)
{
scanf("%d",&cin_value);
list[cin_value]++;
max_value=max(max_value,cin_value);
}
long long res_m=max_selectnum(max_value);
printf("%lld\n",res_m);
return 0;
}