动态规划初探(含完整代码实现)

动态规划:思想

算法:有模板

基本思想:一个大问题可以分成若干个小问题,并且各个问题的解决思路是类似的,

也可以通过小问题的解得到大问题的解,小问题之间往往不是相互独立的 eg.每个阶段做的决策相互影响

Q; q1---q2---q3

小问题之间往往是相互独立的---分治 eg.堆排序

递归--->一种编程技巧,分治和动态规划可以用递归来写

性质:最优子结构性质(通过小问题的解得到大问题的解) 重叠子问题性质

最短路径具有,u→v的最短距离可以拆成min u→s+min s→v

最长路径不具有

重叠子问题性质:

复杂度O(2^n) 近似看作满二叉树,执行次数就是结点个数

如上图斐波拉数列,需要剪枝:把已经计算过的值用数组记录下来,下次再让算的时候就不用递归了

含代码案例 。 这个数组叫做备忘录数组/记忆化数组 然后这个方法叫动态规划,也叫记忆化搜索

含代码实现:

1.基于递归实现:记忆化搜索/备忘录写法 从上往下

2.基于非递归实现:DPtable----->一般用循环(递推)实现非递归 从下往上

#include<iostream>
using namespace std;
int f[100];   //记忆化数组/备忘录数组/状态数组  f[i]不为-1时表示fib[i]的值

/*
int fib(int n)
{
  if (n <= 2) return 1;
  else return fib(n - 1) + fib(n - 2);
}
*/

//备忘录写法:记忆化搜索
int fib(int n)
{
  if (n <= 2) return 1;

  else
  {
    if (f[n] != -1) return f[n];
    else
    {
      f[n]= fib(n - 1) + fib(n - 2);
      return f[n];
    }
  }
  
}

int main()
{

  f[1] =f[2]= 1;
  for (int i = 1; i < 100; i++)
  {
    f[i] = -1; //表示fib[i]未被计算过
  }
  cout<<fib(5);
  
}

动态规划的解题思路:

1.确定是否是动态规划问题: 最优子结构性质 重叠子问题性质

2.确定状态数组:状态/阶段=子问题 比如上述的f[i]=j就是状态数组,因为斐波拉数列本身就是要求所有位的数字

而f[i]=j表示第i个数字是j,这就是子问题

状态数组: 储存子问题的结果数组 如何确定?

(1)经验

(2)技巧:分析出子问题,子问题改成陈述句描述,其中的参数就是状态数组的下标,答案就是状态数组每一项的值

子问题:第i项的值是几? 第i项的值是x. dp[i]=x

….i….j(二维)答案是x dp [i] [j] = x

(3)先用记忆化搜索(类似于递归树)的方式自上而下分析:递归---->然后你要分析,优化,剪枝

得到的数组就是 记忆化数组/状态数组

3.确定状态转移方程: f[i]=f[i-1]+f[i-2]

(1)经验

(2)求中间状态法:

初始状态: f[0]=0 f[1]=1

中间状态: f[i]

最终状态: f[n]-->答案

假设第i个状态之前的值都是已知的,根据题意分析第i个状态中怎么求

f[i]=f[i-1]+f[i-2]

(3)先用记忆化搜索的方式自上而下分析: 递归的代码中暗含了转移方程

4.写代码: 备忘录写法/DPtable写法

题型: 线性dp,区间dp 背包问题 状压dp 树型dp

例题:

用1 3 5组成n有多少个方案 ⇒ 用 1 3 5组成n有x种方案---->(1,3,5是常数,不能做下标)状态数组:dp[n]=x

dp[0]=0 dp[1]=1 dp[2]=1 dp[3]=2 dp[4]=3 dp[5]=5 dp[6]=8……

4=(3)+1或(1)+3

dp[i]=? 状态方程dp[i]=dp[i-1]+dp[i-3]+dp[i-5] 最后一步选的数只能是1、3、5中的一个

for(int i=6;i<=n;i++)

{ dp[i]=dp[i-1]+dp[i-3]+dp[i-5] }

滚动数组----降维 dp[i][j]----->dp[i]

总结为什么想到用动态规划

例题:找状态方程

01背包

背包问题: 01背包

有一些物品,每种物品有自己的价值v和重量w,有一个背包,背包可以装入物品,装入的物品的重量的上限是c

选择任意物品放到背包里面,在不超过背包重量上限的情况下,所选物品的总价值最大是多少?

每种物品只有一个

贪心:按单价贪心 (X) 物品不能拆分成半个,你要选只能选整个

思路一: 暴力枚举

n个物品,要么在背包里(标为1)要么不在背包里(0) 去枚举所有的情况 --- 2^n种情况,找到不超过背包重量且价值最大的

问:我怎么去枚举?

发现可以用树型结构表示 总共2^n种情况

通过递归实现 每个"结点"的实现逻辑都是一样的

二:代码优化--具有重叠子结构问题---->记忆化搜索

把编号,剩余容量相等的树进行剪枝<------因为代码中的形参是编号和剩余容量

三:找到状态转变方程,化为非递归

状态数组:dp[c][n]=x ,在背包容量还剩c时,从1~~n个物品中选,价值最大是x

dp[i][j]=x ,在背包容量还剩i时,从1~~j个物品中选,价值最大是x

初始状态:dp[0][j]=0 dp[i][0]=0

中间状态: dp[i][j] 最终状态: dp[c][n]

状态转移方程:

dp[i][j] = max(dp[i][j-1], dp[i-w[j]][j -1] + v[j]); //从前往后

代码:

// dp[0][j]=0 dp[i][0]=0 全局

for (int i = 1; i <= c; i++) //重量

{

for (int j = 1; j <= n; j++) //编号

{

if (i>w[j])

{

dp[i][j] = max(dp[i][j-1], dp[i-w[j]][j -1] + v[j]); //从前往后

}

else dp[i][j] = dp[i][j-1];

}

}

时间复杂度:O(n*c) 空间复杂度O(n*c)--(二维数组降维)--->O(n)

四.状态数组的降维

dp[i][j]---->dp[j]

分析(dp[i][j] = max(dp[i] [j-1], dp[i-w[j]] [j -1] + v[j]))

我求..[j]可以把[j-2] [j-3]丢掉吧 更新dp[i]

可知每次计算需要用到上一列前面的值,所以倒着遍历

dp[i][j]=x ,在背包容量还剩i时,从1~~j个物品中选,价值最大是x

dp[i]=x (含义不变) 在背包容量还剩i时,从1~~j个物品中选,价值最大是x

dp[i]=max(dp[i],dp[i-w[j]]+v[j]) //我本身的值和更新过后的值他俩谁大

总结:1.先通过暴力枚举,然后发现有重叠子问题2.添加备忘录数组,改成了记忆化搜索(递归)

3.找到了状态数组,改成动态规划版本(非递归形式)

4.滚动数组降维

//01背包问题:
// 有一些物品,每种物品有自己的价值v和重量w,有一个背包,背包可以装入物品,装入的物品的重量的上限是c
//选择任意物品放到背包里面,在不超过背包重量上限的情况下,所选物品的总价值最大是多少?

//方法:暴力
int w[100];  //w[i]表示第i个物品重量是
int v[100];

#include<iostream>
using namespace std;
#include <cmath>

// 优化:重叠子问题,用记忆化数组 / 状态数组
//在背包容量还剩c时,在1~i个物品中,选择判断i是否放入背包中, 返回放入和不放入两种情况的最大值为x  f[c][i]
int dp[100][100];

int dp_[100]; //降维,下标表示重量


//在背包容量还剩c时,在1~i个物品中,选择判断i是否放入背包中, 返回放入和不放入两种情况的最大价值    (对于i来说)就是放入和不放入这两种情况,如果放入,
//时间复杂度  O(2^n)                                                                                这种情况最大值  v[i] + Find(c - w[i], i - 1)
int Find(int c,int i)
{
  int sum1 = 0, sum2 = 0;
  if (i == 0) return 0;
  if (dp[c][i] != -1) return dp[c][i];
  if (w[i] <= c)
  {
    sum1 = v[i] + Find(c - w[i], i - 1);  //sum1放入背包的情况,最大值
  }
  //不放入背包的情况,最大值
  sum2 = Find(c,i - 1);

  dp[c][i]= max(sum1, sum2);
  return dp[c][i];

}



int Find_(int c,int i)
{
  int sum1 = 0, sum2 = 0;
  if (i == 0) return 0;
  if (w[i] <= c)
  {
    sum1 = v[i] + Find(c - w[i], i - 1);  //sum1放入背包的情况,最大值
  }
  //不放入背包的情况,最大值
  sum2 = Find(c,i - 1);

  return max(sum1, sum2);

}

//找到状态方程,将递归转化为非递归
void solution(int c,int n)
{
  // dp[0][j]=0   dp[i][0]=0 全局
  for (int i = 1; i <= c; i++)  //重量
  {
    for (int j = 1; j <= n; j++) //编号
    {
      if (i>w[j])
      {
        dp[i][j] = max(dp[i][j-1], dp[i-w[j]][j -1] + v[j]);   //从前往后
      }
      else dp[i][j] = dp[i][j-1];
    }
  }

}
//优化空间复杂度O(n*c)--->O(n) 二维数组降维
void solution_(int c,int n)
{
  // dp[0][j]=0   dp[i][0]=0 全局
  for (int i = 1; i <= n; i++)   //编号
  {
    for (int j = c; j >= w[i]; j--)  //倒着遍历,如果能更新,找到最大值。如果不能更新,就不变
    {
      dp_[j] = max(dp_[j], dp_[j - w[i]] + v[i]);
    }
  }

}


int main()
{
  int n;
  int c;//背包承受总重量
  cin >>n>> c;

  for (int i = 1; i <= n; i++)
  {
    cin >> v[i] >> w[i];
  }
  
  for (int i = 1; i <= c; i++)
  {
    for (int j = 1; j <= n; j++)
    {
      dp[i][j] = -1;
    }
  }

  
  //int ans=Find(c,n);
  //cout << ans<<endl;

  //solution(c, n);
  //cout << dp[c][n];

  solution_(c, n);
  cout << dp_[c];
}
/*测试示例
* 5 10
* 6 2
* 3 5
* 5 4
* 4 2
* 6 3
*/

最短路径---Floyd算法

Floyd算法---->多源最短路径 图可以带负边权,但是不能有负回路 eg.如下,5没有最短路径

1.最短路径具有最优子结构 和 重叠子问题 性质 基于动态规划

dijkstra算法:dis[k]+a[i][j]<dis[j]----> dis[j]=dis[k]+a[k][j]

尝试让任意两点之间距离变短的方法:在两点之间引入中转点K

采用邻接矩阵存图 a[i][j]=INF,a[i][i]=0(考虑到可能让a[i][i]自指) a[i][i]=INF(简单图,无自边)

最终使用n个点做中转点时,i点和j点的最短路径长度是多少?

最终使用n个点做中转点时,i点和j点的最短路径长度x。

2.状态数组:dp[n][i][j]=x;

初始状态:dp[0][i][j] = a[i][j]

中间状态:dp[k][i][j]=y //用前k个点(1~~k) 做中转点时,i点和j点的最短路径长度是y

最终状态:dp[n][i][j]=x //用前n个点做中转点时,i点和j点的最短路径长度是x

3.状态转移方程

若dp[0][i][j]~~dp[k-1][i][j] 已知,求dp[k][i][j]

dp[k][i][j]=min(dp[k-1][i][j] , dp[k-1][i][k] + dp[k-1][k][j] );

4.写代码:

降维优化:可以通过二维的滚动数组降维

dp[i][j]=y. dp[k][i][j]=y 用前k个点(1~~k) 做中转点,i点和j点的最短路径长度是y

/*测试示例 (笔记中有图)
* 5 7
* ABCDE
* A C 1
* A E 10
* B E 5
* B D 1
* C B 1
* C E 7
* D E 1
*/

#include<iostream>
#include<cmath>
using namespace std;
#define INF 10000
//图的存储结构-邻接矩阵
int p[100][100];   //p[x][y]=w  编号x到编号y的边权值为w。无边是INF
int n, m;
char Data[100];  //顶点存储的数据

//最短路径具有最优子结构  和 重叠子问题 性质   基于动态规划
//尝试让任意两点之间距离变短的方法:在两点之间引入中转点K
int dp[100][100][100]; //dp[a][b][c]=x  从编号1~~a中选择中转点 , 使得编号b到编号c的距离是最短的


int Find(char x)
{
  for (int i = 1; i <= n; i++)
  {
    if (Data[i] == x) return i;
  }
  return -1;
}

void Floyd()
{
  //初始化状态数组
  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= n; j++)
    {
      dp[0][i][j] = p[i][j];
    }
  }

  //状态转移方程: dp[k][i][j]=min(dp[k-1][i][j]  ,  dp[k-1][i][k] + dp[k-1][k][j] );
  for (int k = 1; k <= n; k++)    
  {
    for (int i = 1; i <= n; i++)

    {
      for (int j = 1; j <= n; j++)
      {
        dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]);  //dp[k]实际上就是在dp[k-1]的基础上是否选择一个编号为k的点作为中转点
      }
    }
  }

}

//*****************降维:三维数组降为二维(滚动)数组******************
int dp_[100][100];

void Floyd_()  //优化 
{
  //初始化状态数组
  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= n; j++)
    {
      dp_[i][j] = p[i][j];
    }
  }

  //状态转移方程:  Floyd算法的核心代码
  for (int k = 1; k <= n; k++)  
  {
    for (int i = 1; i <= n; i++)
    {
      for (int j = 1; j <= n; j++)
      {
        dp_[i][j] = min(dp_[i][j], dp_[i][k] + dp_[k][j]);
      }
    }
  }

}



int main()
{
  cin >> n >> m;
  for (int i = 1; i <= n; i++)
  {
    cin >> Data[i];
  }

  //初始化邻接矩阵
  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= n; j++)
    {
      p[i][j] = INF;
      if (i == j) p[i][j] = 0;
    }
  }

  for (int i = 1; i <= m; i++)
  {
    char x, y;
    int w;
    cin >> x >> y >> w;
    int xi=Find(x);
    int yi = Find(y);
    p[xi][yi] = w;

  }
  

/* Floyd();
  
  //直接找到可选1~~n个编号作为中转点就行了,此时一定是最短路径
  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= n; j++)
      {
      if (dp[n][i][j] == INF||i==j) continue;
      cout << "点"<<Data[i]<<"到点"<<Data[j]<<"的最短路径为" <<dp[n][i][j]<<endl;
      }
  }*/

  Floyd_();

  for (int i = 1; i <= n; i++)
  {
    for (int j = 1; j <= n; j++)
    {
      if (dp_[i][j] == INF || i == j) continue;
      cout << "点" << Data[i] << "到点" << Data[j] << "的最短路径为" << dp_[i][j] << endl;
    }
  }
  
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值