动态规划:思想
算法:有模板
基本思想:一个大问题可以分成若干个小问题,并且各个问题的解决思路是类似的,
也可以通过小问题的解得到大问题的解,小问题之间往往不是相互独立的 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;
}
}
}

2万+

被折叠的 条评论
为什么被折叠?



