C++动态规划超详细总结

动态规划

首先来介绍一下动态规划,但我不想用过于官方的语言来介绍。动态规划是一种思想,它常用于最优解问题(即所有问题包括所有子问题的解为最优解),它有点像递推,是在已知问题的基础上解决其他问题。这种思想较为复杂,也是很多 OIer 的痛。

解题步骤

  1. 把一个问题拆分成很多小问题

  1. 找出最初的状态(即上文“在已知问题的基础上”的已知部分)

  1. 建立状态转移方程(即上文“解决其他问题”)

其实状态转移方程有点像找规律,通过前面的规律推出后面。

例题讲解

我们先从最简单经典的跳台阶问题开始。

台阶问题

题目描述

有 N 级的台阶,你一开始在底部,每次可以向上迈最多K级台阶(1或2级),问到达第N级台阶有多少种不同方式。

输入格式

两个正整数N,K。

输出格式

一个正整数,为不同方式数。

样例 #1

样例输入 #1

5 2

样例输出 #1

8

台阶问题的解法

思路

首先题目的意思就是 N 阶台阶,每次可以迈 1或2 阶,问有几种迈的方法。

这里我们不妨设一个函数 为结果。

每阶台阶可以向上走 1或2阶,那么第 N 阶台阶一定是从 N-1 或者 N-2 阶台阶来的,第 N-1 或 N-2 阶台阶也一定是从 N-3/N-2 或 N-3/N-4 来的,以此类推。

那么,状态转移方程为

dp[N]=dp[N-1]+dp[N-2]

怎么样,是不是很简单?

难度提升!

代码

#include<iostream>
using namespace std;
int m,dp[3],n;
int main(){
  cin >> n;
  for(int i=1;i<=n;i++){
    cin >> m;
    dp[0]=1;
    dp[1]=1;
    if(m<2) break;
    for(int j=2;j<m;j++){
      dp[j]=dp[j-1]+dp[j-2];
    }
    cout << dp[m-1] << endl;
  }
  return 0;
}

田忌赛马

题目描述

我国历史上有个著名的故事: 那是在2300年以前。齐国的大将军田忌喜欢赛马。他经常和齐王赛马。他和齐王都有三匹马:常规马,上级马,超级马。一共赛三局,每局的胜者可以从负者这里取得200银币。每匹马只能用一次。齐王的马好,同等级的马,齐王的总是比田忌的要好一点。于是每次和齐王赛马,田忌总会输600银币。

田忌很沮丧,直到他遇到了著名的军师――孙膑。田忌采用了孙膑的计策之后,三场比赛下来,轻松而优雅地赢了齐王200银币。这实在是个很简单的计策。由于齐王总是先出最好的马,再出次好的,所以田忌用常规马对齐王的超级马,用自己的超级马对齐王的上级马,用自己的上级马对齐王的常规马,以两胜一负的战绩赢得200银币。实在很简单。

如果不止三匹马怎么办?这个问题很显然可以转化成一个二分图最佳匹配的问题。把田忌的马放左边,把齐王的马放右边。田忌的马A和齐王的B之间,如果田忌的马胜,则连一条权为200的边;如果平局,则连一条权为0的边;如果输,则连一条权为-200的边……如果你不会求最佳匹配,用最小费用最大流也可以啊。 然而,赛马问题是一种特殊的二分图最佳匹配的问题,上面的算法过于先进了,简直是杀鸡用牛刀。现在,就请你设计一个简单的算法解决这个问题。

输入格式

第一行一个整数n,表示他们各有几匹马(两人拥有的马的数目相同)。第二行n个整数,每个整数都代表田忌的某匹马的速度值(0 <= 速度值<= 100)。第三行n个整数,描述齐王的马的速度值。两马相遇,根据速度值的大小就可以知道哪匹马会胜出。如果速度值相同,则和局,谁也不拿钱。

数据规模

对于20%的数据,1<=N<=65;

对于40%的数据,1<=N<=250;

对于100%的数据,1<=N<=2000。

输出格式

仅一行,一个整数,表示田忌最大能得到多少银币。

样例 #1

样例输入 #1

3
92 83 71
95 87 74

样例输出 #1

200

田忌赛马问题的解法

这道题除了 DP ,还有简单的做法,我直接放代码,但为了学习 DP,我还是讲一下 DP 做法。

简单解法

//田忌赛马
#include<iostream>
#include<algorithm>
using namespace std;
int n,qsp[2010],tsp[2010];
int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>tsp[i];
    }
    for(int i=0;i<n;i++){
        cin>>qsp[i];
    }
    sort(qsp,qsp+n);
    sort(tsp,tsp+n);
    int tmin=0,tmax=n-1,qmin=0,qmax=n-1,jb=0;
    for(int i=0;i<n;i++){
        if (tsp[tmin]>qsp[qmin]){
            jb+=200;
            tmin++;
            qmin++;
        }
        else if(tsp[tmax]>qsp[qmax]){
            jb+=200;
            tmax--;
            qmax--;
        }
        else if(tsp[tmin]<qsp[qmax]){
            jb-=200;
            qmax--;
            tmin++;
        }
    }
    cout<<jb<<endl;
    return 0;
}

这段代码大家应该能看懂,我不做讲解。

DP 做法

看到这道题,大家可能毫无头绪(做题时不要损坏设备)

首先,田忌拥有比赛的“主动权”,因为他可以根据齐王出的马来出马。可以假设齐王出马的顺序是从强到弱,那么田忌出马应该是最强或最弱。用 f[i,j] 表示齐王出了 i 匹较强的马和田忌出了 j 匹较强的马。i-j 表示较弱的马比赛之后田忌获得的利益。

那么状态转移方程是

f[i][j]=max(f[i-1][j]+g[n-i+j+1][i],f[i-1][j-1]+g[j][i])

其中g[i][j] 表示田忌的马和齐王的马分别按照由强到弱的顺序排序之后,田忌的第 i 匹马和齐王的第 j 匹马赛跑所能取得的盈利,胜为 200 ,负为 −200 ,平为 0。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

const int N=2001,INF=-2e+8;
int a[N],b[N],g[N][N],f[N];

bool Cmp(int n1,int n2) {return n1>n2;}

int main()
{
    int n,Ans,i,j; scanf("%d",&n);
    for (i=1;i<=n;++i) scanf("%d",&a[i]);
    for (i=1;i<=n;++i) scanf("%d",&b[i]);
    sort(a+1,a+n+1,Cmp),sort(b+1,b+n+1,Cmp);
    for (i=1;i<=n;++i)
        for (j=1;j<=n;++j)
        {
            if (a[i]>b[j]) g[i][j]=200;
            else if (a[i]==b[j]) g[i][j]=0;
                 else g[i][j]=-200;
        }
    for (i=1;i<=n;++i) f[i]=INF;
    for (i=1;i<=n;++i)
    {
        f[i]=f[i-1]+g[i][i];
        for (j=i-1;j>0;--j)
            f[j]=max(f[j]+g[n-i+j+1][i],f[j-1]+g[j][i]);
        f[0]=f[0]+g[n-i+1][i];
    }
    Ans=f[1];
    for (i=2;i<=n;++i) Ans=max(Ans,f[i]);
    printf("%d\n",Ans);
    return 0;
}

怎么样,还能理解吗?

[ 真题 ] 纪念品

动态规划的难度和精髓在于状态转移方程。 ——鲁迅(我没说过这句话)

接下来这道题会让大家知道什么是真正的状态转移方程。

题目描述

小伟突然获得一种超能力,他知道未来 TN 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。

每天,小伟可以进行以下两种交易无限次

1. 任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;

2. 卖出持有的任意一个纪念品,以当日价格换回金币。

每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。

T 天之后,小伟的超能力消失。因此他一定会在第 T 天卖出所有纪念品换回金币。

小伟现在有 M 枚金币,他想要在超能力消失后拥有尽可能多的金币。

输入格式

第一行包含三个正整数 T, N, M,相邻两数之间以一个空格分开,分别代表未来天数 T,纪念品数量 N,小伟现在拥有的金币数量 M

接下来 T 行,每行包含 N 个正整数,相邻两数之间以一个空格分隔。第 i 行的 N 个正整数分别为 P_{i,1},P_{i,2},……,P_{i,N},其中 P_{i,j} 表示第 i 天第 j 种纪念品的价格。

输出格式

输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。

样例 #1

样例输入 #1

6 1 100
50
20
25
20
25
50

样例输出 #1

305

样例 #2

样例输入 #2

3 3 100
10 20 15
15 17 13
15 25 16

样例输出 #2

217

提示

【输入输出样例 1 说明】

最佳策略是:

第二天花光所有 100 枚金币买入 5 个纪念品 1;

第三天卖出 5 个纪念品 1,获得金币 125 枚;

第四天买入 6 个纪念品 1,剩余 5 枚金币;

第六天必须卖出所有纪念品换回 300 枚金币,第四天剩余 5 枚金币,共 305 枚金币。

超能力消失后,小伟最多拥有 305 枚金币。

【输入输出样例 2 说明】

最佳策略是:

第一天花光所有金币买入 10 个纪念品 1;

第二天卖出全部纪念品 1 得到 150 枚金币并买入 8 个纪念品 2 和 1 个纪念品 3,剩余 1 枚金币;

第三天必须卖出所有纪念品换回216 枚金币,第二天剩余1枚金币,共 217 枚金币。

超能力消失后,小伟最多拥有 217 枚金币。

纪念品问题的解法

思路

这道题其实是动态规划和完全背包问题的结合。

我们进行 t−1 轮完全背包:

把今天手里的钱当做背包的容量

把商品今天的价格当成它的消耗,

把商品明天的价格当做它的价值

每一天结束后把总钱数加上今天赚的钱,直接写背包模板即可。

另: 在这道题中,我们可以把商品和钱看成同样的东西,因为题目中说了:可以当天买当天卖,所以不必考虑跨天的买卖,只需考虑当天的即可,这满足动态规划对于最优化原理和无后效性的要求,可以大胆地购买。

除第一天只有购入过程、最后一天只有售出过程外,每天都有售出与购入两个过程。两个过程互不干扰。

为获得更多的“资金”,不妨令每日的售出过程先于购入过程。

每天的购入过程与次日的售出过程(差价)构成一次完全背包。或者说,完全背包是在“第 X.5 天”进行的。

定义:

f[i]为用 i 元钱去购买商品所能盈利的最大值(不含成本

状态转移方程: f[j]=max(f[j],f[jprice[i][k]]+price[i][k+1]−price[i][k]);

代码

#include <iostream>
#include <memory.h>
using namespace std;
const int N = 101;
const int M = 10001;
int n, m, t, price[N][N], f[M];
int main()
{
    cin >> t >> n >> m;
    for(int i = 1; i <= t; i++)
        for(int j = 1; j <= n; j++)
            cin >> price[j][i];
          //读入每种商品每天的价格
    for(int k = 1; k < t; k++)
    {
        memset(f, 0, sizeof f);//每轮开始前都要制零
        for(int i = 1; i <= n; i++)
            for(int j = price[i][k]; j <= m; j++)//完全背包,正着循环
                f[j] = max(f[j], f[j - price[i][k]] + price[i][k + 1] - price[i][k]);
      
        m += f[m];//加上盈利的钱,进入下一轮买卖
    }
    cout << m;
    return 0;
}

这样就好了(

最后

这篇博客到这里也就结束了,今天主要是介绍了《简单》的动态规划问题(bushi,题目提交地址可以看我的 OJ。(

拜拜~~

猜你喜欢

转载自blog.csdn.net/m0_64036070/article/details/128723056