算法笔记之蓝桥杯pat系统备考(3)

算法笔记之蓝桥杯&pat系统备考(2)
多训练、多思考、多总结٩(๑•̀ω•́๑)۶

八、深搜和广搜

8.1DFS

dfs是一种枚举完所有完整路径以遍历所有情况的搜索方法,可以理解为每次都是一条路走到黑的犟种。
以老朋友斐波那契额数列为例,F(0)和F(1)可以视为死胡同,对于F(n)可以再走F(n-1)和F(n-2),依次类推。
简单应用:01背包

#include<iostream>
using namespace std;
const int maxn = 210;
int maxv = 0, n, m;
int w[maxn], v[maxn];void dfs(int index, int sumW, int sumC){if(index == n){//完成对n间物品的选择,来到了死胡同if(sumW <= m && sumC > maxv) maxv = sumC;//出现不超过背包容量,且价值更大的方案则更新return;}dfs(index + 1, sumW + w[index], sumC + v[index]);//放入第index物品试试dfs(index + 1, sumW, sumC);//选择不放入第index件物品 
}int main(){scanf("%d%d", &n, &m);for(int i = 0; i < n; i++){scanf("%d%d", w + i, v + i);}dfs(0, 0, 0);printf("%d", maxv);return 0;
} 

每件物品有两种选择,则上述代码的时间复杂度为O(2n)。当前想法是对所有物品确定后才判断是否超重,其实很有可能在过程中就已经超重了,完全可以在对每个物品的判断中都先确定不会超重的基础上继续选择。

#include<iostream>
using namespace std;
const int maxn = 210;
int maxv = 0, n, m;
int w[maxn], v[maxn];void dfs(int index, int sumW, int sumC){if(index == n) return;if(sumW + w[index] <= m){if(sumC + v[index] > maxv) maxv = sumC + v[index];//更新最大价值 dfs(index + 1, sumW + w[index], sumC + v[index]);//放入试试}dfs(index + 1, sumW, sumC);//选择不放入 
}int main(){scanf("%d%d", &n, &m);for(int i = 0; i < n; i++){scanf("%d%d", w + i, v + i);}dfs(0, 0, 0);printf("%d", maxv);return 0;
} 

这种通过题目条件的限制来节省计算量的方法称为剪枝。
上述的01背包中是一类常见的DFS问题的解决方法,即给定一个序列,枚举这个序列的所有子序列(可以不连续)。
牛刀小试:
蓝桥杯算法提高VIP-01背包
蓝桥杯-分糖果
蓝桥杯-飞机降落
蓝桥杯-小朋友崇拜圈
蓝桥杯-正则问题
1103 Integer Factorization
蓝桥杯-买瓜

8.2BFS

BFS广度优先搜索,当碰到岔道口时,先依次访问该岔道口能直接到达的所有结点,然后再访问这些结点能直接到达的结点,以此类推。
在这里插入图片描述
BFS一般由队列实现,且总是按层次的 顺序进行遍历,其基本写法为:

void bfs(int s){queue<int> q;//定义队列qq.push(s);//把起点s入队while(!q.empty()){取出队首元素top;访问队首元素top;把队首元素出队;把top的下一层结点中没入队的结点全部入队,并设置为已入队; }
} 

在这里插入图片描述
即若干个(> 0)相邻的1为一个块,所谓相邻包括上下左右四个位置

#include<iostream>
#include<queue>
using namespace std;
const int maxn = 100;
struct node{//结点(x, y) int x,y;
}Node;int n, m, a[maxn][maxn], flag[maxn][maxn] = {0};
int X[4] = {0, 0, -1, 1};//增量数组 
int Y[4] = {-1, 1, 0, 0};//左 右 上 下 bool judge(int x, int y){//判断是否需要访问点(x, y) if(x < 0 || x >= n || y < 0 || y >= m) return false;//越界,非法坐标if(!a[x][y] || flag[x][y]) return false;//当前位置为0或者已经访问过,则跳过该点即可return true; 
} void bfs(int x, int y){queue<node> q;//辅助队列Node.x = x;//当前节点为(x, y) Node.y = y; flag[x][y] = 1;q.push(Node);//当前结点入队while(!q.empty()){//队列非空时循环 node top = q.front();//取出队首元素q.pop();//队首元素出队for(int i = 0; i < 4; i++){//得到上下左右的点坐标 int newX = top.x + X[i];int newY = top.y + Y[i];if(judge(newX, newY)){//若需要访问则入队 Node.x = newX;Node.y = newY;q.push(Node);flag[newX][newY] = 1;} } }
} int main(){int ans = 0;//总块数 scanf("%d%d", &n, &m);//矩阵规模 for(int i = 0; i < n; i++){for(int j = 0; j < m; j++){scanf("%d", &a[i][j]);}}for(int i = 0; i < n; i++){//枚举每一个位置 for(int j = 0; j < m; j++){if(a[i][j] && !flag[i][j]){//当前位置是1且不在其他已计数的块中 ans++;//块数加一 bfs(i, j);//访问整个块,把它们的位置都标记为1已出现 }}}printf("%d", ans);return 0;
}

在这里插入图片描述

在这里插入图片描述
块数问题练习

  • tips:涉及到最小操作步骤问题,优先考虑bfs(联想洪水流向四面八方,最先流到即最小操作步骤),例如八数码问题
  • 可以借助map绑定状态和相应次数

bfs是对于其相邻结点,再依次递推对相邻结点的相邻结点进行处理。所有它的应用也是对于周边下手的类型,八数码问题空格按照某种规则向相邻位置移动,数1的块数问题是一坨相邻的1算是一块。

十一、动态规划

11.1动态规划的递归写法和递推写法

引入选读

11.1.1啥是动态规划

动态规划(Dynamic Programming ,DP)是一种用来解决一类最优化问题的算法思想。简言之,动态规划把一个复杂的问题分解为若干个子问题,通过综合子问题的最优解来得到原问题的最优解。其中,动态规划会把每个求解过的子问题的解记录下来,这样下次用到同样的子问题时可以直接使用之前记录的结果,而非笨笨地重复计算。不过嘛,虽然动态规划采用该方式来提高计算效率,但不能说这就是动态规划的核心(后续会说明这一点)。
一般可使用递归或者递推的写法来实现动态规划,其中递归写法又称为记忆化搜索。

11.1.2动态规划的递归写法

先来瞄瞄递归写法,通过这块呢我们可以理解下动态规划是咋记录子问题的解,来避免下次遇到相同的子问题的重复计算的。
以老朋友斐波那契数列为例,定义为f0 = 1, f1 = 1, fn = fn-1+fn-2(n >= 2)。直接递归实现:

int f(int n){if(n == 0 || n == 1) return 1;return f(n-1) + f(n-2);
}

会发现包含了许多重复的计算,时间复杂度是O(2n),即每次都会计算f(n-1)和f(n-2)这两个分支,基本不能负担n较大的情况。
为了避免重复计算,可以开一个一维数组dp,用以保存已经计算过的结果,其中dp[n]记录f(n)的结果,用dp[n] = -1表示f(n)当前还未被计算过。

int dp[maxn];

可以再递归中判断dp[n]是否为-1,若不是,说明已经计算过,直接返回dp[n]即可;否则,按照递归式进行递归

int f(int n){if(n == 0 || n == 1) return 1;//递归边界 if(dp[n] != -1) return dp[n]; //已计算过,直接返回结果,无需重复计算 esle{dp[n] = f(n - 1) + f(n - 2);//计算f(n),并保存到dp[n] return dp[n];//返回f(n)的结果 }
}

上述把已经计算过的内容记录下来,之后再用到时相同内容时直接使用上次计算的结果即可,省去了无效计算,也就是所谓的记忆化搜索的由来。通过记忆化搜索,时间复杂度从O(2n)降到了O(n)。
通过该例子可以引申出一个概念:若一个问题可以被分解为若干个子问题,且这些子问题会重复出现,则称这个问题具有重叠子问题(Overlapping Subproblems)。动态规划通过记录重叠子问题的解,使后续碰到相同的子问题时直接使用之前记录的结果,来避免大量重复计算。故,一个问题必须拥有重叠子问题,才能去使用动态规划解决。

11.1.3动态规划的递推写法

以经典的数塔问题为例,

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

在上面的样例中,从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 73875 的路径产生了最大权值。

  • 输入格式

第一个行一个正整数 r r r ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

  • 输出格式

单独的一行,包含那个可能得到的最大的和。

  • 样例 #1

  • 样例输入 #1

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
  • 样例输出 #1
30
  • 提示

【数据范围】
对于 100 % 100\% 100% 的数据, 1 ≤ r ≤ 1000 1\le r \le 1000 1r1000,所有输入在 [ 0 , 100 ] [0,100] [0,100] 范围内。(end)

首先如果尝试穷举所有路径,再去得到路径上数字和的最大值,则由于每层中的数字都会有两条分支路径,时间复杂度为O(2n)。显然n一大就跑不起来了。
一开始,从第一层的7出发,按7->3->1的路线来到1,并枚举从1出发的到达最底层的所有路径。但是当按7->8->1的路线再次来到1时,又会去枚举从1出发到达最底层的所有路径,这就导致了从1出发的到达最底层的所有路径时就把路径上能产生的最大和记录下来,这样当再次访问到1这个数字时就可以直接获取这个最大值,避免重复计算。
基于上述分析,不妨令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和,例如说dp[3][2]就是图中的1到最底层的路径最大和。再定义这个数组后,dp[1][1]就射最终目标答案。若想求出“从位置(1,1)到达最底层的最大和”dp[1][1],则一定要先求出它的两个子问题“从位置(2,1)到达最底层的最大和dp[2][1]”和“从位置(2,2)到达最底层的最大和dp[2][2]”,即进行了一次决策:走数字7的左下还是右下。则dp[1][1]就是dp[2][1]和dp[2][2]的较大值加上7,写为式子:

dp[1][1] = max(dp[2][1], dp[2][2]) + f[1][1]

进一步可以归纳为,若想求除dp[i][j],则要先求出它的两个子问题“从位置(i + 1, j)到达最底层的最大和dp[i + 1, j]”和“从位置(i + 1, j + 1)到达最底层的最大和dp[i + 1][j + 1]”,即进行了一次决策:走位置(i,j)的左下还是右下。

dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + f[i][j]

把dp[i][j]称为问题的状态,上式称为状态转移方程,把状态dp[i][j]转译为dp[i+1][j]和dp[i+1][j+1]。可发现,状态dp[i][j]只和第i+1层的状态有关,与其他层状态无关,则层号i的状态可以有层号i+1的两个子状态得到。关于边界,可以发现数塔的最后一层的dp值总是等于元素本身,即dp[n][j]==f[n] j ,把这种可直接确定其结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组。
如此就能从最底层各位置的dp值开始,不断往上求出每一层各位置的dp值,最后得到dp[1][1],即为所求。

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1010;
int a[maxn][maxn], dp[maxn][maxn];
int main(){int r;	scanf("%d", &r);for(int i = 1; i <= r; i++){for(int j = 1; j <= i; j++){scanf("%d", &a[i][j]);}}for(int i = 1; i <= r; i++){//边界dp[r][i] = a[r][i];//最底层木有路,路径和为本身 }for(int i = r; i >= 1; i--){//从第n-1层不断往上计算出dp[i][j]for(int j = 1; j <= i; j++){dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + a[i][j];//状态转移方程}}printf("%d", dp[1][1]);return 0;
}

显然,使用递归也可以实现上面的例子

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1010;
int r, a[maxn][maxn], dp[maxn][maxn];
int f(int x, int y){if(x == r) return a[x][y];if(dp[x][y] != -1) return dp[x][y];else{dp[x][y] = max(f(x + 1, y), f(x + 1, y + 1)) + a[x][y];return dp[x][y];}
}
int main(){scanf("%d", &r);for(int i = 1; i <= r; i++){for(int j = 1; j <= i; j++){scanf("%d", &a[i][j]);if(i == r) dp[i][j] = a[i][j];else dp[i][j] = -1;}}printf("%d", f(1, 1));return 0;
} 

两者的区别在于:使用递推写法的计算方式是自底向上(Bottom-up Approach),即从边界开始,不断向上解决问题,直到解决了目标问题;而使用递归写法的计算方式是自顶向下(Top-down Approach),即从目标问题开始,把它分解为子问题的组合,直到分解至边界为止。
通过上述例子再引申出一个概念:若一个问题的最优解可以由其子问题的最优解有效地构造出来,则称这个问题拥有最优子结构(Optimal Substructure)。最优子结构保证了动态规划中原问题的最优解可以由子问题的最优解推导而来。一个问题必须拥有最优子结构,才能使用动态规划来解决。例如数塔问题中,每一个位置的dp值都可以由它的两个子问题推导得到。
需要指出,一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决

  • 分治vs动态规划

    • 相同点:都是把问题分解为子问题,再合并子问题的解得到原问题的解
    • 不同点:
      • 分治法分解出的子问题是不重叠的,因而分治法解决的问题不拥有重叠子问题。例如归并排序和快速排序都是分别处理左序列和右序列,再把左右序列的结果合并,过程中不出现重叠子问题,故它们使用的都是分治法。
        动态规划解决的问题拥有重叠子问题。
      • 分治法解决的问题不一定是最优化问题,而动态规划解决的问题一定是最优化问题。
  • 贪心vs动态规划

    • 相同点:贪心和动态规划都要求原问题必须拥有最优子结构。
    • 不同点:
      • 贪心法采用的计算方式类似于上面介绍的“自顶向下”,但是并不等待子问题求解完毕后再选择使用哪一个,而是通过一种策略直接选择一个子问题去求解,没被选择的子问题就不去求解了,直接丢掉。换言之,贪心总是只在上一步选择的基础上继续选择,故整个过程以一种单链的流水方式进行,显然这种所谓的“最优选择”的正确性需要用归纳法证明。例如对数塔问题而言,贪心法从最上层开始,每次选择左下和右下两个数字中较大的一个,一只到最底层得到最后结果,显然不一定得到最优解。
        动态规划不管是采用自底向上还是自顶向下的计算方式,都是从边界开始向上得到目标问题的解。换言之,它总是会考虑所有子问题,并选择继承能得到最优结果的那个,对暂时没有继承的子问题,由于重叠子问题的存在,后期可能会再次考虑它们,还有机会成为全局最优的一部分,不需要放弃。所以,贪心就是只要进行了选择,就不后悔;动态规划要看哪个选择笑到了最后,暂时的领先不算啥。

11.2最大连续子序列和

给定一个数字序列a1, a2, ……an,求i,j(1<=i<=j<=n)使得ai+……+aj最大,输出这个最大和。
令状态dp[i]表示为a[i]作为结尾的连续序列的最大和,则只有两种情况:

  1. 该最大和的连续序列只有一个元素,即以a[i]开始,以a[i]结尾
  2. 该最大和的连续序列有多个元素,即从前面某处a[p]开始(p < i),一直到a[i]结尾
    得到状态转移方程dp[i] = max(a[i], dp[i - 1] + a[i])
    这个式子只和i和i之前的元素有关,且边界为dp[0] = a[0],则从小到大枚举i,即可得到整个dp数组,数组中的最大值即为最大连续子序列和。
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 10010;
int a[maxn], dp[maxn];//a[i]存放序列,dp[i]存放以a[i]为结尾的连续序列的最大和 
int main(){int n, ans;scanf("%d", &n);for(int i = 0; i < n; i++)//读入序列 scanf("%d", &a[i]);ans = dp[0] = a[0];//边界for(int i = 1; i < n; i++){dp[i] = max(a[i], dp[i - 1] + a[i]);//状态转移方程 if(dp[i] > ans) ans = dp[i];} printf("%d", ans);return 0;
} 

在这里插入图片描述
顺带再了解下无后效性这个概念。
状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。
例如宇宙的历史可以看作一个关于时间的线性序列,对每一个时刻而言,宇宙的现状就是这个时刻的状态,显然宇宙过去的信息蕴含在当前状态中,并只能通过当前状态来影响下一个时刻的状态,则从这个角度来说宇宙的关于时间的状态并无后效性。对于刚说的最大连续子序列和而言,每次计算状态dp[i],都只会涉及dp[i - 1]而不直接用到dp[i-1]蕴含的历史信息。
对动态规划可解的问题而言,总会有很多设计状态的方式,但并不是所有状态都具有无后效性,则必须设计一个拥有无后效性的状态以及相应的状态转移方程,否则动态规划就无法得到正确结果。事实上呀,如何设计状态和状态转移方程,才是动态规划的核心,而它们也是动态规划最难的地方

11.3最长不降子序列(LIS)

最长不降子序列(Longest Increasing Sequence, LIS):在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。例如对于序列a = {1, 2, 3, 4, -1, -2, 7, 9}的最长不下降序列是{1, 2, 3, 4, 7, 9},长度为5.
令dp[i]表示以a[i]为结尾的最长不下降子序列长度,有两种可能:

  • 存在a[i]之前的元素a[j],满足a[j] <= a[i]且dp[j] + 1 > dp[i](即把a[i]跟在a[j]为结尾的LIS后面能比当前记录的以a[i]为结尾的LIS更长),则更新为a[i]跟在a[j]后的LIS长度
  • 若a[i]之前的元素都比a[i]大,则只能a[i]自己形成一条LIS,长度为1,即这个子序列中只有一个a[i]

状态转移方程

dp[i] = max (1, dp[j]+1)(0<=j<=i-1)
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1010;
int a[maxn], dp[maxn];//dp[i]以a[i]为结尾的最长不降子序列长度 
int main(){int n, ans = -1;scanf("%d", &n);for(int i = 0; i < n; i++){scanf("%d", &a[i]);}for(int i = 0; i < n; i++){dp[i] = 1;//默认只有a[i] for(int j = 0; j < i; j++){if(a[i] >= a[j] && (dp[j] + 1 > dp[i])) dp[i] = dp[j] + 1;//状态转移方程,用以更新dp[i]} ans = max(ans, dp[i]);}printf("%d", ans);return 0;
}

11.4最长公共子序列(LCS)

最长公共子序列(Longest Common Subsequence, LCS):给定两个字符串(或数字序列)A和B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)。例如,kid和killed的最长公共子序列为kid,长度为3.
令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始),根据a[i]和b[j]的情况,分为两种决策:

  • a[i] == b[j],则字符串a和字符串b的LCS增加了一位,即有dp[i][j] = dp[i-1][j-1] + 1
  • a[i] != b[j],则字符串a的i号位和字符串b的j号位之前的LCS无法延长,故dp[i][j]继承dp[i-1][j]和dp[i][j-1]中的较大值

状态转移方程为:
在这里插入图片描述

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
int main(){string a, b;cin >> a >> b;a = " " + a;b = " " + b;int la = a.size(), lb = b.size();int dp[la + 1][lb + 1];for(int i = 0; i < la; i++){dp[i][0] = 0;} for(int i = 0; i < lb; i++){dp[0][i] = 0;}for(int i = 1; i < la; i++){for(int j = 1; j < lb; j++){if(a[i] == b[j]) {dp[i][j] = dp[i - 1][j - 1] + 1;}else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);}}printf("%d", dp[la - 1][lb - 1]);return 0;
}

11.7背包问题

11.7.1多阶段动态规划问题

有一类动态规划可解的问题,可以描述为若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,一般把这类问题称为多阶段动态规划问题。

11.7.2 01背包问题

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为v的背包,如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。(所谓01的由来)
令dp[i][v]表示对于前i件物品装入容量为v的背包中所能获得的最大价值。
对于第i件物品的选择,有两种策略:

  • 不放第i件物品,问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,dp[i-1][v]
  • 放第i件物品,问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,即dp[i-1][v-w[i]] + c[i]

状态转移方程为:
在这里插入图片描述

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 210, maxv = 5010;
int w[maxn], v[maxn], dp[maxn][maxv];
int main(){int n, allV;scanf("%d%d", &n, &allV);for(int i = 1; i <= n; i++){scanf("%d%d", &w[i], &v[i]);}for(int i = 0; i <= allV; i++){//边界,没有物品时对于所有容量都只能有0价值 dp[0][i];}for(int i = 1; i <= n; i++){//状态转移方程 for(int j = 1; j <= allV; j++){if(j < w[i]) dp[i][j] = dp[i - 1][j];else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);//有两种决策,不放或者放 }}printf("%d", dp[n][allV]); return 0;
} 

其实手动模拟会发现,每次更新都只需要上一行的内容,考虑只维持一维数组。注意dp[v - w[i]]用到的是旧值,所以为了防止被刷新为新值出错,必须使用逆序枚举!
在这里插入图片描述

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 210,maxv = 5010;
int w[maxn], v[maxn], dp[maxv];
int main(){int n, m, ans = -1;scanf("%d%d", &n, &m);for(int i = 1; i <= n; i++){scanf("%d%d", &w[i], &v[i]);}for(int i = 0; i <= m; i++){//边界 dp[i] = 0;}for(int i = 1; i <= n; i++){for(int j = m; j >= w[i]; j--){//滚动数组 dp[j] = max(dp[j], dp[j - w[i]] + v[i]);}}for(int i = 0; i <= m; i++){ans = max(ans, dp[i]);}printf("%d", ans);return 0;
}

动态规划是如何避免重复计算的问题在01背包中很明显。在一开始暴力枚举每件物品放或者不放背包时,忽略了一个特性:第i件物品放或者不放而产生的最大值完全可以由前i-1件物品的最大值而决定,而暴力忽略了这点。
此外,01背包中的每个物品都可以看作一个阶段,这个阶段中的状态有dp[i][0]~dp[i][V],均由上一个阶段的状态得到。其实,对能够划分阶段的问题而言,都可以尝试把阶段作为状态的一维,使得更方便地得到满足无后效性的状态。另一方面,若当前设计的状态不满足无后效性,不妨把状态进行升维,即增加一维或多维来表示相应的信息,这样也许就能满足无后效性了

11.7.3完全背包问题

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为v的背包,如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。
dp[i][v]表示对前i件物品进行选取放入容量为v的背包中能获得的最大价值。
对于每种物品也是有两种策略:

  • 不放第i件物品,则dp[i][v] = dp[i-1][v]
  • 放入第i件物品,是转移到dp[i][v-w[i]](01背包中每个物品只能选择一次,选择放入第i件物品后必须转移到dp[i-1][v-w[i]]),因为每种物品在容量允许的情况下能够放任意件,已经放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保持大于等于0为止。
    在这里插入图片描述
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 35, maxv = 210;
int w[maxn], c[maxn], dp[maxn][maxv];
int main(){int n, m;scanf("%d%d", &m, &n);for(int i = 1; i <= n; i++){scanf("%d%d", &w[i], &c[i]);}for(int i = 0; i <= m; i++){//边界 dp[0][i] = 0;}for(int i = 1; i <= n; i++){//状态转移方程for(int j = 1; j <= m; j++){if(j < w[i]) dp[i][j] = dp[i - 1][j];else dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + c[i]);}}printf("max=%d", dp[n][m]);return 0;
}

dp[i][v- w[i]]需要用新值更新
==> 一维形式必须正序枚举

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 35, maxv = 210;
int w[maxn], c[maxn], dp[maxv];
int main(){int n, m;scanf("%d%d", &m, &n);for(int i = 1; i <= n; i++){scanf("%d%d", &w[i], &c[i]);}for(int i = 0; i <= m; i++){dp[i] = 0;}for(int i = 1; i <= n; i++){for(int j = w[i]; j <= m; j++){dp[j] = max(dp[j], dp[j - w[i]] + c[i]);}}printf("max=%d", dp[m]);return 0;
}

动态规划的方案输出,记录每一步选择了哪个策略,然后从最终态(例如背包问题中的总容量v)倒着判断即可

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1e4 + 10, maxv = 110;
int w[maxn], dp[maxv], choice[maxn][maxv], ans[maxn];bool cmp(int a, int b){return a > b;
} int main(){int n, m, num = 0;scanf("%d%d", &n, &m);for(int i = 1; i <= n; i++){scanf("%d", &w[i]);}sort(w + 1, w + n + 1, cmp);//从大到小排序 for(int i = 0; i <= m; i++){//边界,常规01背包 dp[i] = 0;}for(int i = 1; i <= n; i++){for(int j = m; j >= w[i]; j--){dp[j] = max(dp[j], dp[j - w[i]] + w[i]);if(dp[j] > dp[j - w[i]] + w[i]) choice[i][j] = 0;//不放else choice[i][j] = 1; //注意等于的时候也放哦 }} if(dp[m] != m) printf("No Solution");else{//从小到大输出选择的物品int tn = n, tm = m;while(tm){//从终态倒着判断if(choice[tn][tm] == 1){ans[num++] = w[tn];tm -= w[tn];}tn--;}for(int i = 0; i < num; i++){if(i) printf(" ");printf("%d", ans[i]);}}return 0;
} 

11.8总结

在这里插入图片描述
其中,递推顺序是正推还是逆推,是从小到大还是从大到小.

  1. 最大连续子序列和
    dp[i]表示以a[i]为结尾的连续序列的最大和
    dp[i] = max(a[i], dp[i - 1] + a[i])
  2. 最长不下降子序列(LIS)
    dp[i]表示以a[i]为结尾的最长不下降子序列长度
    dp[i] = max (1, dp[j]+1)(0<=j<=i-1)
  3. 最长公共子序列(LCS)
    dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始)
    在这里插入图片描述
  4. 数塔DP
    dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径上所能得到的最大和。
    dp[i][j] = max(dp[i + 1][j] , dp[i+1][j+1]) + a[i][j]
  5. 01背包
    dp[i][v]表示前i件物品进行选择装入容量为v的背包中能获得的最大价值
    在这里插入图片描述
  6. 完全背包
    dp[i][v]表示前i件物品进行选择装入容量为v的背包中能获得的最大价值
    在这里插入图片描述
    前三个都是关于序列或字符串的问题(一般而言,“子序列”可以不连续,“子串”必须连续)。
    前两个的设计状态的方法都是“令dp[i]表示以a[i]为结尾的……”,其中……即为原问题的描述,再分析a[i]的情况来进行状态转移。
    第三个的LCS原问题本身就有二维性质,使用了“令dp[i][j]表示i号位和j号位之间……”的状态设计方式,其中……为原问题的描述。
    现在可以小结一下:
  • 当题目和序列或字符串(记为A)有关时,可以考虑把状态设计为下面两种形式,再根据端点特点去考虑状态转移方程
    • 令dp[i]表示以A[i]为结尾(或开头)的……
    • 令dp[i][j]表示A[i]到A[j]区间的……

接着盘4-6,发现它们的状态设计都包含了某种“方向”。数塔DP设计中从点(i, j)出发到最底层的最大和,背包问题设计为dp[i][v]表示前i件物品恰好放入容量为v的背包中能获得的最大价值。
再次小结出一类动态规划问题的状态设计方法:

  • 分析题目中的状态需要几维来表示,然后对其中的每一维采取下面的某一个表述:
    • 恰好为
    • 前i

在每一维的含义设置完毕之后,dp数组的含义就可以设置为"令dp数组表示恰好为i(或前i)、恰好为j(或前j)……的……"其中后一个……为原问题的描述。接下来就可以通过端点的特点去考虑状态转移方程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/785007.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker-compose管理工具的使用

华子目录 容器编排工具docker composecompose介绍compose使用的三个步骤docker-compose.yml文件案例compose具有管理应用程序整个生命周期的命令 docker compose安装安装条件在Linux系统上安装composedocker compose卸载 docker compose运用演示修改compose配置&#xff0c;添加…

【手册】——mq延迟队列

目录 一、背景介绍二、思路&方案三、过程1.项目为啥用延迟队列&#xff1f;2.项目为啥用三方延迟队列&#xff1f;3.项目中为啥用rabbitmq延迟队列&#xff1f;4.rabbitmq延迟队列的安装5.rabbitmq的延迟队列配置方式5.1.exchange配置5.2.queues配置5.3.exchange和queues的…

初识C++ · 入门(2)

目录 1 引用 1.1引用的概念 1.2 引用的特性 2 传值&#xff0c;传引用的效率 3 引用和指针的区别 4 内联函数 4.1 内联函数的定义 4. 2 内联函数的特性 5 关键字auto 5.1关于命名的思考 5.2 关于auto的发展 5.3 auto使用规则 6 范围for的使用 7 空指针 1 引用 …

win10如何开启麦克风权限,win10麦克风权限设置

手机下载软件后,总是会跳出各种权限需要,例如访问通讯录、读取位置信息、启动相机等等。电脑上的应用也有这些权限设置,比如说玩游戏、直播、或录制视频时,我们需要打开麦克风权限,否则无法进行交流和录音。但是,win10如何开启麦克风权限呢?针对这个问题,小编已整理了两…

《自动机理论、语言和计算导论》阅读笔记:p115-p138

《自动机理论、语言和计算导论》学习第 6 天&#xff0c;p115-p138 总结&#xff0c;总计 24 页。 一、技术总结 1.associativity and comutativity (1)commutativity(交换性): Commutativity is the property of an operator that says we can switch the order of its ope…

比KMP简单的Manacher

P3805 【模板】manacher - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) “没时间悼念KMP了&#xff0c;接下来上场的是Manacher&#xff01;” 什么是Manacher? 历史背景&#xff1a; 1975 年&#xff0c;一个叫 Manacher 的人发明了这个算法&#xff0c;所以叫Manacher 算…

财务管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目持续更新中..... 2024年计算机毕业论文&#xff08;设计&#xff09;学生选题参考合集推荐收藏&#xff08;包含Springboot、jsp、ssmvue等技术项目合集&#xff09; 目录 1. …

02-JDK新特性-接口新特性

接口新特性 接口组成更新概述 接口的组成 常量 public static final String ZERO "0";抽象方法 public abstract void dance();默认方法&#xff08;JAVA8新增&#xff09; public default void dance(){}静态方法&#xff08;JAVA8新增&#xff09; public stat…

leecode 331 |验证二叉树的前序序列化 | gdb 调试找bug

计算的本质是数据的计算 数据的计算需要采用格式化的存储&#xff0c; 规则的数据结果&#xff0c;可以快速的按照指定要求存储数据 这里就不得不说二叉树了&#xff0c;二叉树应用场景真的很多 本题讲的是&#xff0c;验证二叉树的前序序列化 换言之&#xff0c;不采用建立树的…

Logback日志框架常见配置

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl logback简介 Logback是一个高性能、功能强大的日志框架&#xff0c;专为Java应用程序设计。它由Log4j的创始人Ceki Glc创建&#xff0c;并被视为Log4j的继承者和改进版。Lo…

3D Web轻量化平台HOOPS Web Platform在大型水利工程中的应用和价值

随着科技的不断进步和互联网的普及&#xff0c;数字化技术在各个领域的应用日益广泛&#xff0c;大型水利工程也不例外。作为一种先进的3D Web轻量化引擎&#xff0c;HOOPS Web Platform在大型水利工程中扮演着重要的角色&#xff0c;为工程设计、施工管理、运行维护等方面提供…

Ubuntu部署BOA服务器

BOA服务器概述 BOA是一款非常小巧的Web服务器&#xff0c;源代码开放、性能优秀、支持CGI通用网关接口技术&#xff0c;特别适合用在嵌入式系统中。 BOA服务器主要功能是在互联嵌入式设备之间进行信息交互&#xff0c;达到通用网络对嵌入式设备进行监控&#xff0c;并将反馈信…

Go的数据结构与实现【Binary Search Tree】

介绍 本文用Go将实现二叉搜索树数据结构&#xff0c;以及常见的一些方法 二叉树 二叉树是一种递归数据结构&#xff0c;其中每个节点最多可以有两个子节点。 二叉树的一种常见类型是二叉搜索树&#xff0c;其中每个节点的值都大于或等于左子树中的节点值&#xff0c;并且小…

Unbtun-arach64架构安装PySide2(python3.6)

aarch平台是无法通过pip安装PySide2的&#xff0c;同时利用源码下载一直报错 1. 我是python3.6.9&#xff0c;在官网上找到对应的PySide2版本 5.15.2.所以首先在官网下载Qt5.15.2的源码&#xff1a;https://download.qt.io/archive/qt/5.15/5.15.2/single/ 2. 编译qt环境 aar…

Seata(分布式事务实例环境搭建)

文章目录 1.基本介绍1.引出Seata2.问题分析 2.Seata的安装和配置1.解压到d盘2.修改D:\seata\conf\file.conf文件1.修改事务组2.修改日志存储模式为db3.修改数据库&#xff08;MySQL5.7&#xff09;连接信息4.创建seata数据库5.复制db_store.sql的内容&#xff0c;创建需要的表6…

Web实例_报表开发01-基于HTML进行报表呈现

Web实例_报表开发01-基于HTML进行报表呈现 报表开发是一种在利用了软件的基础上, 针对不同类型的报表, 进行开放的工作。 而以报表的方式, 将相关的内容、数值呈现出来的话, 则会起到更好的概况作用。 再加上, 报表开发工作是依托于计算机来完成的, 因此在效率、完整性等方面…

红酒:分类视角下的红酒品质评估与标准制定

在红酒的世界中&#xff0c;品质的评估与标准的制定对于维护消费者权益、促进行业健康发展具有重要意义。云仓酒庄雷盛红酒作为业界持续发展品牌&#xff0c;从分类的视角出发&#xff0c;对红酒品质进行了多方的评估&#xff0c;并积极参与制定相关的标准。 首先&#xff0c;从…

优思学院|工程经理应如何利用PDCA循环?

作为工程经理&#xff0c;理解PDCA&#xff08;计划-执行-检查-行动&#xff09;质量管理循环程序对于确保项目质量和持续改进是十分重要。 PDCA 最早由美国质量管理专家 Walter A. Shewhart (1939)提出&#xff0c;其后被戴明博士&#xff08;Dr. Deming&#xff09;所采用、…

【Linux】权限理解

权限理解 1. shell命令以及运行原理2. Linux权限的概念3. Linux权限管理3.1 文件访问者的分类&#xff08;人&#xff09;3.2 文件类型和访问权限&#xff08;事物属性&#xff09;3.2.1 文件类型3.2.2 基本权限 3.3 文件权限值的表示方法3.4 文件访问权限的相关设置方法3.4.1 …

基于energy score的out-of-distribution数据检测,LeCun都说好 | NerulPS 2020

论文提出用于out-of-distributions输入检测的energy-based方案&#xff0c;通过非概率的energy score区分in-distribution数据和out-of-distribution数据。不同于softmax置信度&#xff0c;energy score能够对齐输入数据的密度&#xff0c;提升OOD检测的准确率&#xff0c;对算…