动态规划

在学习强化学习的过程中,涉及了利用动态规划的思想对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)=\min \{f(n-1), f(n-5), f(n-11)\}+1 \]

我们能这样干,取决于问题的性质:求出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

posted @ 2020-10-11 17:19  兀凯奇  阅读(170)  评论(0)    收藏  举报