很多动态规划题目要求返回的答案不是一个简单数值,而是一个具体的方案
1.利用动态规划表生成决策路径,如题目一、题目二、题目三
2.有时候需要增加额外的路径收集结构,如题目四
对这一类的题目来说,动态规划是最重要的,得到具体方案只是一个比较简单的处理技巧。
下面通过题目来加深理解。
题目一
测试链接:https://www.nowcoder.com/practice/4727c06b9ee9446cab2e859b4bb86bb8
分析:这道题如果是单纯的求最长公共子序列的长度是比较简单的,但是现在需要得到具体的子序列我们可以通过动态规划表来回溯回去,回溯的起点就是动态规划表的终点。如果当前点对于两个字符串对应的字符相等,则代表这个字符是最长公共子序列中的字符,且当前点是由斜上方的点求得;如果对应字符不相等,则代表当前点是由正上方或左方的点求得,如果正上方或左方的点能够比较最大值则取较大值的点作为当前点的下一步,如果两个点的值相等选择左方的点为当前点的下一步。直到回溯完成。代码如下。
#include <iostream>
using namespace std;
char str1[5000];
char str2[5000];
int dp[5001][5001] = {0};
int main(void){
scanf("%s", &str1);
scanf("%s", &str2);
int length1 = 0;
while (str1[length1] != '\0')
{
++length1;
}
int length2 = 0;
while (str2[length2] != '\0')
{
++length2;
}
for(int i = 1;i <= length1;++i){
for(int j = 1;j <= length2;++j){
if(str1[i-1] == str2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
if(dp[length1][length2] == 0){
printf("%d", -1);
}else{
int row = length1, column = length2;
char ans[dp[length1][length2]];
int index = dp[length1][length2];
while(index != 0){
if(str1[row-1] == str2[column-1]){
ans[--index] = str1[row-1];
--row;
--column;
}else{
if(row - 1 >= 0 && dp[row][column-1] == dp[row][column]){
--column;
}else{
--row;
}
}
}
for(int i = 0;i < dp[length1][length2];++i){
printf("%c", ans[i]);
}
}
return 0;
}
题目二
测试链接:https://leetcode.cn/problems/smallest-sufficient-team/
分析:这道题先解决规模最小的必要团队的最小规模,观察到数据量,可以采用状压dp。将需求的技能清单中的技能从字符串映射为编号,每个成员的技能掌握可以由字符串映射的编号再映射到每一位上即每个成员的技能掌握可以由一个整型数字表示。然后采用严格位置依赖的解法,求解出最小规模的人数。在从动态规划的终点开始回溯,如果当前点的上方点的最小规模和当前点的最小规模一样,则代表没有选取当前成员,当前点的下一点为正上方点;否则选取了当前成员,当前点的下一个点是排除掉当前成员掌握技能的状态的上一行的点。代码如下。
class Solution {
public:
int dp[61][(1 << 16)] = {0};
int person[60];
vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
int skill_length = req_skills.size();
int people_length = people.size();
map<string, int> m;
for(int i = 0;i < skill_length;++i){
m.insert(pair<string, int>(req_skills[i], i));
}
int temp;
map<string, int>::iterator pos;
for(int i = 0;i < people_length;++i){
temp = 0;
for(int j = 0;j < people[i].size();++j){
pos = m.find(people[i][j]);
if(pos != m.end()){
temp |= (1 << ((*pos).second));
}
}
person[i] = temp;
}
for(int i = 1;i < (1 << skill_length);++i){
dp[0][i] = 61;
}
for(int i = 1;i <= people_length;++i){
for(int j = (1 << skill_length) - 1;j >= 1;--j){
dp[i][j] = min(dp[i-1][j], 1 + dp[i-1][(j & (j ^ person[i-1]))]);
}
}
vector<int> ans;
int row = people_length, column = ((1 << skill_length) - 1);
int num = dp[row][column];
ans.reserve(num);
while (num != 0)
{
if(dp[row-1][column] > dp[row][column]){
ans.push_back(row-1);
--num;
temp = column ^ person[row-1];
column &= temp;
--row;
}else{
--row;
}
}
return ans;
}
};
其中,状压dp详情见拙作 算法【状压dp】。
题目三
测试链接:https://www.luogu.com.cn/problem/T386911
分析:首先按照惯例求出以每个位置为结尾的最长上升子序列的长度,这个求法的最优复杂度是nlogn,也必须采用最优解法,不然数据量会超时。然后从右往左遍历dp数组,如果当前下标的dp值和最长上升子序列中还剩的个数相同,则代表最长上升子序列中对应的位置的数为当前下标对应的数,遍历完数组得到的最长上升子序列,即是字典序最小的最长上升子序列。代码如下。
#include <iostream>
using namespace std;
int n;
int nums[100000];
int End[100000];
int dp[100000];
int ans[100000];
int find(int len, int number){
int l = 0, r = len-1, m;
int ans = -1;
while (l <= r)
{
m = l + (r - l) / 2;
if(End[m] >= number){
ans = m;
r = m - 1;
}else{
l = m + 1;
}
}
return ans;
}
int main(void){
scanf("%d", &n);
for(int i = 0;i < n;++i){
scanf("%d", &nums[i]);
}
int len = 1;
dp[0] = 1;
End[0] = nums[0];
int temp;
int rest = 1;
for(int i = 1;i < n;++i){
temp = find(len, nums[i]);
if(temp == -1){
End[len++] = nums[i];
dp[i] = len;
}else{
End[temp] = nums[i];
dp[i] = temp + 1;
}
rest = max(rest, dp[i]);
}
for(int i = 0;i < rest;++i){
ans[i] = 0;
}
temp = rest;
for(int i = n-1;i >= 0;--i){
if(rest == dp[i]){
ans[--rest] = nums[i];
}
}
for(int i = 0;i < temp-1;++i){
printf("%d ", ans[i]);
}
printf("%d", ans[temp-1]);
return 0;
}
其中,最长上升子序列最优解法详情见拙作 算法【最长递增子序列问题与扩展】。
题目四
测试链接:https://www.luogu.com.cn/problem/P1759
分析:这道题如果只求最长的时间是一个非常简单的物品属性有两维的01背包问题。对于所选物品的具体方案也采用和背包问题一样的遍历,用一个字符串数组和dp数组一起去走for循环,对于可能性的展开,只需要在处理完背包问题之后继续处理字符串即可。下面是空间压缩的解法,代码如下。
#include <iostream>
#include <string>
using namespace std;
int m, v, n;
int data[100][3];
int dp[201][201] = {0};
string path[201][201] = {""};
int main(void){
scanf("%d%d%d", &m, &v, &n);
for(int i = 0;i < n;++i){
scanf("%d%d%d", &data[i][0], &data[i][1], &data[i][2]);
}
int temp;
for(int i = 1;i <= n;++i){
for(int j = m;j >= 1;--j){
for(int k = v;k >= 1;--k){
if(j - data[i-1][0] >= 0 && k - data[i-1][1] >= 0){
temp = data[i-1][2] + dp[j-data[i-1][0]][k-data[i-1][1]];
if(dp[j][k] == temp){
path[j][k] = min(path[j][k], path[j-data[i-1][0]][k-data[i-1][1]] + to_string(i) + " ");
}else if(dp[j][k] < temp){
dp[j][k] = temp;
path[j][k] = path[j-data[i-1][0]][k-data[i-1][1]] + to_string(i) + " ";
}
}
}
}
}
string ans = path[m][v];
printf("%d\n", dp[m][v]);
for(int i = 0;i < ans.size()-1;++i){
printf("%c", ans[i]);
}
return 0;
}