跳转至

Chapter 5 动态规划

知识

alt text

alt text

alt text

alt text

alt text

alt text

alt text

alt text

alt text

模板

1. 背包问题

(1) 0-1 背包

二维朴素:

C++
 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
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int f[N][N];
int v[N], w[N];
int n, m;

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

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            f[i][j] = f[i - 1][j]; // 必可做到的
            if (j >= v[i])
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    }

    cout << f[n][m] << endl;
    return 0;
}

一维优化: j 按 "从大向小" 的顺序枚举

f[i-1][XX] 转移而来, 要用从大到小; 若从 f[i][XX] 转移而来, 那就从小到大 比如这里: f[i - 1][j - v[i]] + w[i]) -> "从大到小"

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
using namespace std;

const int N = 1010;

int f[N];
int v[N], w[N];
int n, m;

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

    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= v[i]; j--) { // j 从大到小
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }

    cout << f[m] << endl;
    return 0;
}

(2) 完全背包

二维朴素: 递推找规律

C++
 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
#include<iostream>
using namespace std;

const int N = 1010;

int f[N][N];
int v[N],w[N];

int main()
{
    int n,m;
    cin >> n >> m;
    for (int i = 1 ; i <= n ;i ++) cin >> v[i] >> w[i];

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <=m; j++) { // 递推找规律
            f[i][j] = f[i-1][j];
            if (j - v[i] >= 0)
                f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i]);
        }
    }

    cout << f[n][m] <<endl;
    return 0;
}

一维优化: j 按 "从小到大" 的顺序枚举

这里: f[i][j - v[i]] + w[i]) -> "从小到大"

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;

const int N = 1010;

int f[N];
int v[N], w[N];

int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];

    for (int i=1; i<=n; i++)
        for (int j=v[i]; j<=m; j++) // j 从小到大
            f[j] = max(f[j], f[j-v[i]] + w[i]);

    cout << f[m] <<endl;
    return 0;
}

(3) 分组背包

二维朴素: 递推无规律, 硬着头皮 hard-coding

C++
 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
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i];

    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);

    cout << f[n][m] << endl;
    return 0;
}

一维优化: 二进制优化 -> 转化成0-1背包 -> "从大到小"枚举

C++
 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
#include<iostream>
using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N]; // 逐一枚举最大是N*logS
int f[M];       // 体积 < M

int main()
{
    cin >> n >> m;
    int cnt = 0; // 分组的组别

    for (int i = 1; i <= n; i ++)// n个 "物品堆"
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1; // 组别里面的个数
        while (k <= s)
        {
            cnt ++; // 组别先增加
            v[cnt] = a * k ; // 整体体积
            w[cnt] = b * k;  // 整体价值
            s -= k; // s 要减小
            k *= 2; // 组别里的个数增加
        }
        // 剩余的一组
        if (s > 0)
        {
            cnt ++;
            v[cnt] = a * s; 
            w[cnt] = b * s;
        }
    }

    n = cnt ; // 枚举次数 由 个数 变成 组别数

    // 01背包一维优化
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= v[i]; j--) // j 从大到小
            f[j] = max(f[j], f[j-v[i]] + w[i]);

    cout << f[m] << endl;
    return 0;
}

(4) 分组背包

二维朴素: 递推无规律, 硬着头皮 hard-coding

C++
 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
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n,m;
int v[N][N],w[N][N],s[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for (int i=1; i<=n; i++) {
        cin >> s[i];
        // 第i个物品组 的 第j个物品 的体积和价值
        for (int j=1; j<=s[i]; j++) cin >> v[i][j] >> w[i][j];
    }

    for (int i=1; i<=n; i++) {
        for (int j=0; j<=m; j++) {
            f[i][j] = f[i - 1][j];
            for (int k=0; k<=s[i]; k++) {
                if (j >= v[i][k]) f[i][j] = max(f[i][j],f[i - 1][j - v[i][k]] + w[i][k]);
            }
        }
    }

    cout<<f[n][m]<<endl;
    return 0;
}

一维优化: j 按 "从大到小" 的顺序枚举

C++
 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
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n,m;
int v[N][N],w[N][N],s[N];
int f[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        for (int j = 1; j <= s[i]; j++) {
            cin >> v[i][j] >> w[i][j];
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 0; j--) { // j 从大到小
            for (int k = 0; k <= s[i]; k ++) {
                if (j >= v[i][k]) {
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
                }
            }
        }
    }

    cout << f[m] <<endl;
    return 0;
}

2. 线性DP

(1) 数字三角形

f[i][j]: 从起点走到[i][j]位置时, 累计的价值max

C++
 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
#include <iostream>
#include <cstring>
using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N],f[N][N];

int main () 
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++) 
            cin >> a[i][j];

    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= i + 1; j++)
            f[i][j] = -INF;

    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i; j++)
            f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];

    int ans = -INF;
    for (int i = 1; i <= n; i++) ans = max(ans, f[n][i]);
    cout << ans << endl;

    return 0;
}

(2) 最长上升子序列

给定一个长度为 N 的数列, 求数值严格单调递增的子序列的长度最长是多少

f[i]: 以第 i 个元素为结尾的最长上升子序列的长度

C++
 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
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n;
int a[N], f[N];

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

    for (int i = 1; i <= n; i ++)
    {
        f[i] = 1; // 只有 a[i] 一个数
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i])
                f[i] = max(f[i], f[j] + 1);
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);

    cout << res <<endl;
    return 0;
}

(3) 最长公共子序列

给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少

C++
 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
#include <iostream>
#include <string>
#include <vector>
using namespace std;

const int N = 1010;

int n, m;
string a, b;
int f[N][N];

int main() 
{
    cin >> n >> m;

    string temp_a, temp_b;
    cin >> temp_a >> temp_b;
    a = " " + temp_a;
    b = " " + temp_b;

    // 注意下标必须从1开始, 不然i-1之类的就error了
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            if (a[i] == b[j])
                f[i][j] = f[i - 1][j - 1] + 1;
            else
                f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        }
    }

    cout << f[n][m] <<endl;
    return 0;
}

[!tip] 学习一下如何让 string 不是 0_indexed 而是 1_indexed string temp_a, temp_b; cin >> temp_a >> temp_b; a = " " + temp_a; b = " " + temp_b;

(4) 最短编辑距离

f[i][j]: 第一个字符串的前 i 个字母 变为 第二个字符串的前 j 个字母 所用的最少操作次数

C++
 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
#include <iostream>
#include <string>
using namespace std;
const int N = 1010;

string a, b; //分别存放两个字符串
string tmp_a, tmp_b;
int f[N][N]; //存储结果

int n, m; //两个字符串的长度

int main()
{
    cin >> n;
    cin >> tmp_a;
    a = " " + tmp_a;

    cin >> m;
    cin >> tmp_b;
    b = " " + tmp_b;

    // 边界,a 字符串的前 i 个字符变为 b 字符串的前 0 个字符,需要 i 步(删除)
    for (int i = 0; i <= n; i++) f[i][0] = i;
    // 边界,a 字符串的前 0 个字符变为 b 字符串的前 i 个字符,需要 i 步(插入)
    for (int i = 0; i <= m; i++) f[0][i] = i;

    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <=m; j++)
        {
            // 情况1 + 情况2
            f[i][j] = min(f[i][j - 1] + 1, f[i - 1][j] + 1);
            // 情况3
            if (a[i] != b[j]) 
                f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
            else 
                f[i][j] = min(f[i][j], f[i - 1][j - 1]);
        }
    }

    // f[n][m] 表示 a 字符传递额前 n 个字符变为 b 字符传递额前 m 个字符需要的最少步骤
    cout << f[n][m] <<endl;
    return 0;
}

3. 区间DP

区间 DP 常用模版:

  1. 第一维通常是枚举区间长度
  2. 一般 len = 1 时用来初始化,枚举从 len = 2 开始
  3. 第二维枚举起点 i (右端点 j 自动获得, j = i + len - 1)
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
for (int len = 1; len <= n; len++) {         // 1D枚举 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 2D枚举 枚举起点
        int j = i + len - 1;                 // 自动出 区间终点

        // 边界初始化
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        // 枚举分割点,构造状态转移方程
        for (int k = i; k < j; k++) {
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

石子合并: 合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价

C++
 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
#include <iostream>
#include <cstring>
using namespace std;

const int N = 307;

int a[N], s[N];
int f[N][N];

int main() 
{
    int n;
    cin >> n;

    // 利用前缀和, 预处理 a[i~j] 的和
    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        s[i] += s[i - 1] + a[i];
    }

    memset(f, 0x3f, sizeof f);

    // 区间 DP 枚举套路: 长度 + 左端点
    for (int len = 1; len <= n; len ++) // 1D枚举 区间长度
    {
        for (int i = 1; i + len - 1 <= n; i ++) // 2D枚举 枚举起点
        {
            int j = i + len - 1; // 自动得到右端点
            if (len == 1) {
                f[i][j] = 0;  // 边界初始化
                continue;
            }

            for (int k = i; k <= j - 1; k ++) { // 分割点k需满足 k <= j - 1
                f[i][j] = min(f[i][j], f[i][k]+f[k + 1][j] + s[j]-s[i-1]);
            }
        }
    }

    cout << f[1][n] << endl;
    return 0;
}

4. 计数类DP

整数划分: 一个正整数n可以表示成若干个正整数之和

本质: 把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数(完全背包问题变形)

Text Only
1
2
3
4
5
6
7
状态: f[i][j] 表示前 i 个整数(1, 2, …, i)恰好拼成 j 的方案数

求方案数:把集合选0个i,1个i,2个i,…全部加起来
    f[i][j] = f[i - 1][j] + f[i - 1][j - i] + f[i - 1][j - 2 * i] + ...;
    f[i][j - i] = f[i - 1][j - i] + f[i - 1][j - 2 * i] + ...;

因此 f[i][j]=f[i−1][j]+f[i][j−i]; (这一步类似完全背包的推导)
C++
 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
// f[i][j] = f[i - 1][j] + f[i][j - i]
#include <iostream>

using namespace std;

const int N = 1e3 + 7, mod = 1e9 + 7;

int f[N][N];

int main() {
    int n;
    cin >> n;

    for (int i = 0; i <= n; i ++) {
        f[i][0] = 1; // 容量为0时,前 i 个物品全不选也是一种方案
    }

    for (int i = 1; i <= n; i ++) {
        for (int j = 0; j <= n; j ++) {
            f[i][j] = f[i - 1][j] % mod; // 特殊 f[0][0] = 1
            if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
        }
    }

    cout << f[n][n] << endl;
}

[!tip] 注意 DP初值问题 1. 求最大值时,当都不选时,价值显然是 0 2. 求方案数时,当都不选时,方案数是 1(即前 i 个物品都不选的情况也是一种方案),所以需要初始化为 1

5. 数位统计DP

计数问题: 给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数

例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次 ......

太难了,要是真考就认栽了

C++
  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <vector>

using namespace std;

int base[10];
int f[10][10];
int g[10][10];

void init()
{
    base[0] = 1;
    for(int i = 1 ; i <= 9 ; i++) base[i] = base[i-1]*10;

    //从00……0 - 99……9 的各位数字有多少个,其中i为数字个数(包含前导零)
    for(int i = 0 ; i <= 9 ; i++) f[1][i] = 1;
    for(int i = 2 ; i <= 9 ; i++)
        for(int j = 0 ; j <= 9 ; j++)
            f[i][j] = f[i-1][j]*10 + base[i-1];

    //从1 - 99……9 的各位数字有多少个,其中i为数字个数(不包含前导零)
    for(int i = 1 ; i <= 9 ; i++) g[1][i] = 1;//循环从1开始
    for(int i = 2 ; i <= 9 ; i++) {
        g[i][0] = g[i-1][0] + f[i-1][0]*9;
        for(int j = 1 ; j <= 9 ; j++)
            g[i][j] = g[i-1][j] + f[i-1][j]*9 + base[i-1];
    }
}

vector<int> dp(int n)
{
    vector<int> ans(10,0); //记录答案
    if(n<=0) return ans; //边界条件

    vector<int> nums;
    while(n) nums.push_back(n%10), n/=10;

    vector<int> last(10,0); //记录前缀中各个数字个数

    //统计1 - 99……9(n-1个9)里面各个数字有多少个
    for(int i = 0 ; i <= 9 ; i++) ans[i] = g[nums.size()-1][i];
    //统计大于10……0(n-1个0) 的树里各个数字有多少个
    for(int i = nums.size()-1 ; i >=0 ; i--) {
        //循环变量i可以表示剩下的数字有多少个
        int x = nums[i];
        for(int j = i==nums.size()-1 ; j < x ; j++) { //第一次循环不能有0
            //前缀部分
            for(int k = 0 ; k <= 9 ; k++)
                ans[k] += last[k] * base[i];
            //当前位置部分
            ans[j] += base[i];
            //后缀部分
            for(int k = 0 ; k <= 9 ; k++)
                ans[k] += f[i][k];
        }
        //更新前缀计数器
        last[x] ++;

        //统计叶子节点(这个数本身)
        if(!i) for(int k = 0 ; k <= 9 ; k++) ans[k] += last[k];
    }
    return ans;
}

vector<int> ask(int a, int b)
{
    auto x = dp(b);
    auto y = dp(a-1);
    vector<int> ans;
    for(int i = 0 ; i <= 9 ; i++) ans.push_back(x[i]-y[i]);
    return ans;
}

void print(vector<int> ans)
{
    for(auto x:ans) printf("%d ",x);
    puts("");
}

bool check(int x)
{
    auto t = ask(x,x);
    vector<int> cnt(10,0);
    while(x) cnt[x%10]++,x/=10;
    for(int i = 0 ; i <= 9 ; i++)
        if(cnt[i] != t[i])
            return false;
    return true;
}

int main()
{
    init();

    //这里是一个DEBUG函数
    // for(int i = 1 ; i <= 1000000 ; i*=10) {
    //     if(!check(i))
    //         printf("ERROR:%d\n",i);
    // }

    int a,b;
    while(cin >> a >> b, a||b) {
        if(a>b) swap(a,b);
        auto t = ask(a,b);
        print(t);
    }

    return 0;
}

6. 状态压缩DP

[!tip] 状态压缩DP本质 所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。

蒙德里安的梦想: 铺砖块

二进制数 表示 每个是否"出头"

C++
 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
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N;

int n, m;
long long f[N][M];
bool st[M];

int main()
{
    while (cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];

        cout << f[m][0] << endl;
    }
    return 0;
}

最短Hamilton路径: 从 0 到 n−1 不重不漏地经过每个点恰好一次 ("一笔画问题")

二进制数 表示 每个node走过与否

C++
 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
#include<iostream>
#include<cstring>

using namespace std;

const int N = 20;
const int M = 1 << 20; // 一共最多有 20个1 种状态

int n;
int w[N][N];
int f[M][N];

int main() 
{
    cin >> n;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> w[i][j];

    // 初始化
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;

    for (int state = 1; state < 1 << n; state++) // 从0到111...11 枚举所有state
    {
        if (state & 1) // state必须要包含起点1
        {
            for (int j = 0; j < n; j++) 
            {
                if (state >> j & 1) // 如果当态前点集包含点j, 那么进行状态转移
                {
                    for (int k = 0; k < n; k++) 
                    {
                        if (state ^ (1 << j) >> k & 1)
                        { 
                            // 如果从当前状态经过点集state中, 去掉点j后, state 仍然包含点k, 那么才能从点k转移到点j
                            // 当然这个 if 也可以不加,因为如果 state 去掉第 j 个点后,state 不包含点 k 了
                            // 那么 f[state ^ 1 << j][k] 必然为正无穷,也就必然不会更新 f[state][j],所以去掉也可以 AC
                            f[state][j] = min(f[state ^ 1 << j][k] + w[k][j], f[state][j]);
                        }
                    }
                }
            }
        }
    }

    cout << f[(1 << n) - 1][n - 1]; // 最后输出 f[111...11][n-1]
    return 0;
}

7. 树形DP

没有上司的舞会: 快乐值max

C++
 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
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 6010;

int n;
int happy[N]; //每个职工的高兴度
int f[N][2]; //上面有解释哦~
int e[N],ne[N],h[N],idx; //链表,用来模拟建一个树
bool has_father[N]; //判断当前节点是否有父节点

void add(int a,int b) { //把a插入树中
    e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}

void dfs(int u) { //开始求解题目
    f[u][1] = happy[u]; //如果选当前节点u,就可以把f[u,1]先怼上他的高兴度

    for(int i = h[u];~i;i = ne[i]){ //遍历树
        int j = e[i];
        dfs(j); //回溯
        //状态转移部分,上面有详细讲解~
        f[u][0] += max(f[j][1],f[j][0]);
        f[u][1] += f[j][0];
    }
}

int main()
{
    scanf("%d",&n);
    for(int i = 1;i <= n;i ++) scanf("%d",&happy[i]); //输入每个人的高兴度

    // 初始化 (经典建树)
    memset(h,-1,sizeof h); //把h都赋值为-1

    for (int i = 1;i < n;i ++) {
        int a,b; //对应题目中的L,K,表示b是a的上司
        scanf("%d%d",&a,&b); //输入~
        has_father[a] = true; //说明a他有上司
        add(b,a); //把a加入到b的后面
    }

    int root = 1; //用来找根节点

    while(has_father[root]) root ++; //找根节点

    dfs(root); //从根节点开始搜索

    printf("%d\n",max(f[root][0],f[root][1]));
    return 0;
}

8. 记忆化搜索

滑雪:

C++
 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 <cstdio>
using namespace std;

const int maxn = 300+10;

int r, c;
int a[maxn][maxn];  // 地形
int dp[maxn][maxn]; // dp的结果

int dx[4] = {-1, 0, 0, 1};
int dy[4] = {0, 1, -1, 0};

int dp_search(int x, int y)
{
    // 如果已经搜索过了, 直接返回即可
    if (dp[x][y]) return dp[x][y];

    // 该点本身就算"区域", 故边界起点是1
    dp[x][y] = 1;

    for (int i=0; i<4; i++)
    {
        int xx = x + dx[i];
        int yy = y + dy[i];

        if (xx>=1 && xx<=r && yy>=1 && yy<=c && a[xx][yy] < a[x][y])
            dp[x][y] = max(dp[x][y], dp_search(xx, yy) + 1); // 递归
    }

    return dp[x][y];
}

void out()
{
    for (int i=1; i<=r; i++)
    {
        for (int j=1; j<=c; j++)
            cout << dp[i][j] << " ";
        cout <<endl;
    }
    puts("");
    for (int i=1; i<=r; i++)
    {
        for (int j=1; j<=c; j++)
            cout << dp_search(i, j) << " ";
        cout <<endl;
    }
}

int main()
{
    cin >> r >> c;
    for (int i=1; i<=r; i++)
        for (int j=1; j<=c; j++)
            cin >> a[i][j];

    int max_len = -1;
    for (int i=1; i<=r; i++)
        for (int j=1; j<=c; j++)
            max_len = max(max_len, dp_search(i,j));
    cout << max_len <<endl;

    // out();

    return 0;
}