动态规划
在学习强化学习的过程中,涉及了利用动态规划的思想对MDP进行训练,所以积累一些动态规划的算法知识
算法理论的内容研读了知乎上这的这个回答:动态规划理论
在他举得例子中:假设您是个土豪,身上带了足够的1、5、10、20、50、100元面值的钞票。现在您的目标是凑出某个金额w,需要用到尽量少的钞票。
依据生活经验,我们显然可以采取这样的策略:能用100的就尽量用100的,否则尽量用50的……依次类推。在这种策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10张钞票。
这种策略称为“贪心”:假设我们面对的局面是“需要凑出w”,贪心策略会尽快让w变得更小。能让w少100就尽量让它少100,这样我们接下来面对的局面就是凑出w-100。长期的生活经验表明,贪心策略是正确的。
但是,如果我们换一组钞票的面值,贪心策略就也许不成立了。如果一个奇葩国家的钞票面额分别是1、5、11,那么我们在凑出15的时候,贪心策略会出错:
15=1×11+4×1(贪心策略使用了5张钞票)
15=3×5(正确的策略,只用3张钞票)
为什么会这样呢?贪心策略错在了哪里?
刚刚已经说过,贪心策略的纲领是:“尽量使接下来面对的w更小”。这样,贪心策略在w=15的局面时,会优先使用11来把w降到4;但是在这个问题中,凑出4的代价是很高的,必须使用4×1。如果使用了5,w会降为10,虽然没有4那么小,但是凑出10只需要两张5元。
在这里我们发现,贪心是一种只考虑眼前情况的策略。
那么,现在我们怎样才能避免鼠目寸光呢?
如果直接暴力枚举凑出w的方案,明显复杂度过高。太多种方法可以凑出w了,枚举它们的时间是不可承受的。我们现在来尝试找一下性质。
重新分析刚刚的例子。w=15时,我们如果取11,接下来就面对w=4的情况;如果取5,则接下来面对w=10的情况。我们发现这些问题都有相同的形式:“给定w,凑出w所用的最少钞票是多少张?”接下来,我们用f(n)来表示“凑出n所需的最少钞票数量”。
那么,如果我们取了11,最后的代价(用掉的钞票总数)是多少呢?
明显 ,\(\operatorname{cost}=f(4)+1=4+1=5\)它的意义是:利用11来凑出15,付出的代价等于f(4)加上自己这一张钞票。现在我们暂时不管f(4)怎么求出来。
依次类推,马上可以知道:如果我们用5来凑出15,cost就是f(10)+1=2+1=3。
那么,现在w=15的时候,我们该取那种钞票呢?当然是各种方案中,cost值最低的那一个!
\(
\begin{array}{l}
\text { - 取11: cost }=f(4)+1=4+1=5 \\
\text { - 取5: cost }=f(10)+1=2+1=3 \\
\text { - 取1: cost }=f(14)+1=4+1=5
\end{array}
\)
显而易见,cost值最低的是取5的方案。我们通过上面三个式子,做出了正确的决策!
这给了我们一个至关重要的启示——f(n)只与f(n-1),f(n-5),f(n-11)相关;更确切地说:
我们能这样干,取决于问题的性质:求出f(n),只需要知道几个更小的f(c)。我们将求解f(c)称作求解f(n)的“子问题”。
这就是DP(动态规划,dynamic programming).
将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解。
下面练习几个动态规划的习题:
Fibonacci
题目描述
The Fibonacci Numbers{0,1,1,2,3,5,8,13,21,34,55...} are defined by the recurrence:
F0=0 F1=1 Fn=Fn-1+Fn-2,n>=2
Write a program to calculate the Fibonacci Numbers.
输入
Each case contains a number n and you are expected to calculate Fn.(0<=n<=30) 。
输出
For each case, print a number Fn on a separate line,which means the nth Fibonacci Number.
样例输入 Copy
1
样例输出 Copy
1
题目是让计算第n个Fibonacci数,用递归实现动态规划,比较简单。
#include <iostream>
using namespace std;
int Fibonacci(int x){
if(x==0) return 0;//设置递归边界
if(x==1) return 1;
return Fibonacci(x-1)+Fibonacci(x-2);
}
int main() {
int n;
while (cin>>n){
cout<<Fibonacci(n)<<endl;
}
return 0;
}
最大连续子序列
题目描述
给定K个整数的序列{ N1, N2, ..., NK },其任意连续子序列可表示为{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= K。最大连续子序列是所有连续子序列中元素和最大的一个,例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20。现在增加一个要求,即还需要输出该子序列的第一个和最后一个元素。
输入
测试输入包含若干测试用例,每个测试用例占2行,第1行给出正整数K( K<= 10000 ),第2行给出K个整数,中间用空格分隔,每个数的绝对值不超过100。当K为0时,输入结束,该用例不被处理。
输出
对每个测试用例,在1行里输出最大和、最大连续子序列的第一个和最后一个元素,中间用空格分隔。如果最大连续子序列不唯一,则输出序号i和j最小的那个(如输入样例的第2、3组)。若所有K个元素都是负数,则定义其最大和为0,输出整个序列的首尾元素。
样例输入 Copy
5
-3 9 -2 5 -4
3
-2 -3 -1
0
样例输出 Copy
12 9 5
0 -2 -1
思路:
1、因为dp[i]要求是必须以A[i]结尾的连续序列,那么只有两种情况:
①这个最大和的连续序列只有一个元素,即以a[i]开始,以A[i]结尾。
②这个最大和的连续序列有多个元素,即从前面的某处a[p](p<i),一直到a[i]结尾。
2、对于①,最大和就是本身a[i];
对于②,最大和是dp[i-1]+a[i],即a[p]+...+a[i-1]+a[i]=dp[i-1]+a[i];
对于这两种情况可以得到递推公式dp[i]=max{a[i],dp[i-1]+a[i]} ,这个式子只和i以及i之前的元素有关,且边界为dp[0]=a[0]。
当最大字串和为负数时,序例全零,用结构体记录一下字串端点。
代码:
#include<iostream>
using namespace std;
const int maxn = 10005;
struct dp_num{
int num,left,right;
}dp[maxn];
int main(){
int n,max1;
int num[maxn];
while (cin>>n&&n) {
for (int i = 0; i < n; i++) {
cin >> num[i];
}
dp[0].num = num[0];//边界 (结果总是确定的,动态规划总是从这些边界出发)
for (int i = 0; i < n; i++) {
if (num[i] > dp[i - 1].num + num[i]) {
dp[i].num = num[i];
dp[i].left = num[i];
dp[i].right = num[i];
} else {
dp[i].num = dp[i - 1].num + num[i];
dp[i].left = dp[i - 1].left;
dp[i].right = num[i];
}
}
int k = 0;
for (int i = 0; i < n; i++) {//dp[i]存放以a[i]结尾的连续序列的最大和,需要遍历i得到最大的才是结果
if (dp[i].num > dp[k].num) {
k = i;
}
}
if(dp[k].num>0){
cout<<dp[k].num<<" "<<dp[k].left<<" "<<dp[k].right<<endl;
}else{
cout<<"0 "<<num[0]<<" "<<num[n-1]<<endl;
}
}
return 0;
}
最长上升子序列
题目描述
一个数列ai如果满足条件a1 < a2 < ... < aN,那么它是一个有序的上升数列。我们取数列(a1, a2, ..., aN)的任一子序列(ai1, ai2, ..., aiK)使得1 <= i1 < i2 < ... < iK <= N。例如,数列(1, 7, 3, 5, 9, 4, 8)的有序上升子序列,像(1, 7), (3, 4, 8)和许多其他的子序列。在所有的子序列中,最长的上升子序列的长度是4,如(1, 3, 5, 8)。
现在你要写一个程序,从给出的数列中找到它的最长上升子序列。
输入
输入包含两行,第一行只有一个整数N(1 <= N <= 1000),表示数列的长度。
第二行有N个自然数ai,0 <= ai <= 10000,两个数之间用空格隔开。
输出
输出只有一行,包含一个整数,表示最长上升子序列的长度。
样例输入
7
1 7 3 5 9 4 8
样例输出
4
时间复杂度为O(n*logn)的算法,建立一个堆栈来存储队列,用temp读取数据,当temp大于栈顶时,将temp压入堆栈,否则利用二分法查找堆栈中第一个大于(等于)他的数进行替换,最终,堆栈的长度就是最大上升子序列。他的原理在于,当比栈顶小的数据被压入堆栈后,没有改变长度,但改变了潜能。
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 1005;
int main(){
int n,num[maxn],dp[maxn];
while (scanf("%d",&n)!=EOF){
dp[0]=-1;
int temp,top=0;
for(int i=0;i<n;i++){
cin>>num[i];
temp = num[i];
if(temp>dp[top]){
dp[++top]=temp;
} else{
int low=1,high=top ;
while (low<=high){
int mid=(low+high)/2;
if(dp[mid]<temp){
low=mid+1;
} else{
high=mid-1;
}
}
dp[low]=temp;
}
}
cout<<top<<endl;
}
return 0;
}
最长公共子序列
题目描述
给你一个序列X和另一个序列Z,当Z中的所有元素都在X中存在,并且在X中的下标顺序是严格递增的,那么就把Z叫做X的子序列。
例如:Z=<a,b,f,c>是序列X=<a,b,c,f,b,c>的一个子序列,Z中的元素在X中的下标序列为<1,2,4,6>。
现给你两个序列X和Y,请问它们的最长公共子序列的长度是多少?
输入
输入包含多组测试数据。每组输入占一行,为两个字符串,由若干个空格分隔。每个字符串的长度不超过100。
输出
对于每组输入,输出两个字符串的最长公共子序列的长度。
样例输入 Copy
abcfbc abfcab
programming contest
abcd mnp
样例输出 Copy
4
2
0

浙公网安备 33010602011771号