文章

动态规划中得到具体决策方案

动态规划中通过记录选择路径或逆推状态,实现最优解对应的具体决策方案恢复。

动态规划中得到具体决策方案

动态规划中得到具体决策方案

最长公共子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <vector>
#include <string>

using namespace std;

// 最大字符串长度
const int MAXN = 5001;
// dp[i][j] 表示 s1[0..i-1] 和 s2[0..j-1] 的最长公共子序列长度
vector<vector<int>> dp(MAXN, vector<int>(MAXN));
// 存放最终的最长公共子序列字符数组
vector<char> path(MAXN);
string s1, s2;
// n: s1 长度,m: s2 长度,k: 最长公共子序列长度
int n, m, k;

void computeDp() {
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            if (s1[i - 1] == s2[j - 1]) {
                // 如果当前字符匹配,最长公共子序列在 i-1, j-1 的基础上加 1
                dp[i][j] = 1 + dp[i - 1][j - 1];
            } else {
                // 否则取左边或上边较大的值
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
}

// 逆推恢复一条最长公共子序列路径(不一定是唯一)
void lcs() {
    computeDp();
    k = dp[n][m];     // 最终的最长公共子序列长度保存在 dp[n][m]    

    if (k <= 0) return;
    int len = k;
    int i = n, j = m;

    // 从 dp[n][m] 逆推回去构造子序列
    while (len > 0) {
        if (s1[i - 1] == s2[j - 1]) {
            // 如果当前字符匹配,放入结果中,并向左上角移动
            path[--len] = s1[i - 1];
            i--;
            j--;
        } else if (dp[i - 1][j] >= dp[i][j - 1]) {
            // 向上走
            i--;
        } else {
            // 向左走
            j--;
        }
    }
}

int main() {
    cin >> s1 >> s2;
    n = s1.length();
    m = s2.length();

    lcs();

    if (k == 0) {
        cout << -1 << '\n';
    } else {
        for (int i = 0; i < k; ++i)
            cout << path[i];
        cout << '\n';
    }

    return 0;
}

1125. 最小的必要团队

  • 状压 DP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <climits>

using namespace std;

class Solution {
public:
    // 主函数:返回最小必要团队
    vector<int> smallestSufficientTeam(vector<string> &skills, vector<vector<string>> &people) {
        int n = skills.size();       // 技能总数
        int m = people.size();       // 候选人数
        unordered_map<string, int> skillToBit;

        // 为每个必要技能分配一个唯一编号
        for (int i = 0; i < n; ++i) {
            skillToBit[skills[i]] = i;
        }

        // 每个人掌握技能的 bitmask 表示
        vector<int> skillMasks(m, 0);
        for (int i = 0; i < m; ++i) {
            for (const string &skill: people[i]) {
                if (skillToBit.count(skill)) {
                    skillMasks[i] |= (1 << skillToBit[skill]);
                }
            }
        }

        // dp[i][s] 表示从第 i 个人开始,当前技能状态为 s 时,最少需要多少人才能完成
        vector<vector<int>> dp(m, vector<int>(1 << n, -1));

        // 计算最小团队人数
        int minTeamSize = dfs(skillMasks, m, n, 0, 0, dp);

        // 回溯构造答案
        vector<int> res;
        for (int i = 0, s = 0; s != (1 << n) - 1 && i < m; ++i) {
            // 如果 s 还没凑齐就已经来到最后一个人,说明加入这最后一个人 s 就能凑齐
            // 如果跳过 i 号人后的值和当前值不同,说明选择了 i 号人
            if (i == m - 1 || dp[i][s] != dp[i + 1][s]) {
                res.push_back(i);
                s |= skillMasks[i];  // 技能集合更新
            }
        }

        return res;
    }

    // 记忆化搜索函数
    // arr: 每个人掌握的技能状态,m: 总人数,n: 技能数
    // i: 当前第几个人,s: 当前已覆盖技能状态
    // 返回:从 i 开始覆盖所有技能至少需要多少人
    int dfs(const vector<int> &arr, int m, int n, int i, int s, vector<vector<int>> &dp) {
        // 所有技能已覆盖
        if (s == (1 << n) - 1) return 0;
        // 所有人都用完但技能还没覆盖,返回无效
        if (i == m) return INT_MAX;
        if (dp[i][s] != -1) return dp[i][s];

        // 情况1:不选当前这个人
        int option1 = dfs(arr, m, n, i + 1, s, dp);

        // 情况2:选当前这个人
        int option2 = INT_MAX;
        int next = dfs(arr, m, n, i + 1, s | arr[i], dp);
        if (next != INT_MAX) option2 = 1 + next;

        // 取最小值并记忆化
        dp[i][s] = min(option1, option2);
        return dp[i][s];
    }
};

T386911 最长上升子序列输出解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>

using namespace std;

const int MAXN = 100001;

vector<int> nums(MAXN);       // 输入数组
vector<int> dp(MAXN);         // dp[i]:从 i 开始的最长递增子序列长度
vector<int> min_end(MAXN);    // min_end[i]:长度为 i+1 的递增子序列结尾的最小值(从大到小)
vector<int> res(MAXN);        // 最终答案序列
int n, k;

// min_end 逆序
// 二分查找:在 min_end[0..len-1] 中找 <= target 的最左位置
// 失败时返回 len
int bs(int len, int target) {
    int l = 0, r = len - 1, m;
    while (l <= r) {
        m = (l + r) / 2;
        if (min_end[m] <= target) {
            r = m - 1;
        } else {
            l = m + 1;
        }
    }
    return l;
}

// 构建 dp 表,返回 LIS 的长度
int build_dp() {
    int len = 0;
    for (int i = n - 1; i >= 0; --i) {
        int pos = bs(len, nums[i]);
        if (pos == len) {
            min_end[len++] = nums[i];
            dp[i] = len;
        } else {
            min_end[pos] = nums[i];
            dp[i] = pos + 1;
        }
    }
    return len;
}

// 构建字典序最小的最长递增子序列
// 此处字典序不是常规意义上的字典序,是把每个整数当作一个“单独的字符”来看,而不是按整串组合来比谁字符串更小。
void lis() {
    k = build_dp();
    fill(res.begin(), res.begin() + k, INT_MAX);
    for (int i = 0; i < n; ++i) {
        if (dp[i] == k) {
            // 如果当前位置能作为 LIS 的第一个元素,直接设置
            res[0] = nums[i];
        } else {
            // 如果 nums[i] 能作为 res[k - dp[i]],且比前一位大,则更新
            if (res[k - dp[i] - 1] < nums[i])
                res[k - dp[i]] = nums[i];
        }
    }
    for (int i = 0; i < k; ++i)
        cout << res[i] << (i == k - 1 ? '\n' : ' ');
}

int main() {
    while (cin >> n) {
        for (int i = 0; i < n; ++i) {
            cin >> nums[i];
        }
        lis();
    }
    return 0;
}

P1759 通天之潜水

  • 多维费用背包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <vector>
#include <string>
#include <climits>

using namespace std;

const int MAXN = 105;
const int MAXM = 205;

int a[MAXN], b[MAXN], c[MAXN];
int dp[MAXN][MAXM][MAXM];                 // dp[i][j][k]: 前i个物品,在容量为j、阻力为k的限制下的最大收益
string path[MAXN][MAXM][MAXM];            // 对应的字典序最小的路径

int m, v, n;  // 最大重量、最大阻力、物品数量

void build() {
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            for (int k = 0; k <= v; ++k) {
                dp[i][j][k] = 0;
                path[i][j][k] = "";
            }
        }
    }
}

void compute() {
    string p2;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= m; ++j) {
            for (int k = 0; k <= v; ++k) {
                // 情况1:不选第i个工具
                dp[i][j][k] = dp[i - 1][j][k];
                path[i][j][k] = path[i - 1][j][k];

                // 情况2:尝试选第i个工具(下标从1开始)
                if (j >= a[i] && k >= b[i]) {
                    int preVal = dp[i - 1][j - a[i]][k - b[i]] + c[i];
                    string prePath = path[i - 1][j - a[i]][k - b[i]];
                    if (!prePath.empty()) {
                        p2 = prePath + " " + to_string(i);
                    } else {
                        p2 = to_string(i);
                    }

                    if (dp[i][j][k] < preVal) {
                        dp[i][j][k] = preVal;
                        path[i][j][k] = p2;
                    } else if (dp[i][j][k] == preVal) {
                        if (p2 < path[i][j][k]) {
                            path[i][j][k] = p2;
                        }
                    }
                }
            }
        }
    }
}

int main() {
    while (cin >> m >> v >> n) {
        build();
        for (int i = 1; i <= n; ++i) {
            cin >> a[i] >> b[i] >> c[i];
        }
        compute();
        cout << dp[n][m][v] << "\n";
        cout << path[n][m][v] << "\n";
    }

    return 0;
}
  • 空间压缩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <vector>
#include <string>

using namespace std;

// 最大工具数量
const int MAXN = 101;
// 最大背包容量与最大阻力值
const int MAXM = 201;

int n, m, v; // n:工具数量,m:背包容量限制,v:阻力限制
vector<int> a(MAXN), b(MAXN), c(MAXN); // a[i]:第i个工具的重量,b[i]:阻力,c[i]:增加的停留时间
vector<vector<int>> dp(MAXM, vector<int>(MAXM)); // dp[j][k]:在容量j、阻力k下的最大停留时间
vector<vector<string>> path(MAXM, vector<string>(MAXM)); // path[j][k]:对应最大停留时间的工具选择路径(字典序最小)

// 初始化 dp 和 path 表
void build() {
    for (int i = 0; i <= m; ++i) {
        for (int j = 0; j <= v; ++j) {
            dp[i][j] = 0;
            path[i][j].clear();
        }
    }
}

// 多维费用背包主过程(空间压缩版本)
void compute() {
    for (int i = 1; i <= n; ++i) {
        // 逆序遍历,确保每个工具只被选择一次
        for (int j = m; j >= a[i]; --j) {
            for (int k = v; k >= b[i]; --k) {
                string p2;
                // 构造路径字符串
                if (path[j - a[i]][k - b[i]].empty()) {
                    p2 = to_string(i);
                } else {
                    p2 = path[j - a[i]][k - b[i]] + " " + to_string(i);
                }
                int newTime = dp[j - a[i]][k - b[i]] + c[i];
                if (dp[j][k] < newTime) {
                    // 情况1:可以更新最大停留时间
                    dp[j][k] = newTime;
                    path[j][k] = p2;
                } else if (dp[j][k] == newTime) {
                    // 情况2:停留时间相等,选择字典序更小的路径
                    if (path[j][k].empty() || p2 < path[j][k]) {
                        path[j][k] = p2;
                    }
                }
            }
        }
    }
}

int main() {
    while (cin >> m >> v >> n) {
        build();
        for (int i = 1; i <= n; ++i) {
            cin >> a[i] >> b[i] >> c[i];
        }
        compute();
        cout << dp[m][v] << "\n";
        cout << path[m][v] << "\n";
    }
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权