Chapter 5 动态规划 
知识 
模板 
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 常用模版:
第一维通常是枚举区间长度 
一般 len = 1 时用来初始化,枚举从 len = 2 开始 
第二维枚举起点 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 状态: 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 ; 
}