# 线性 DP
### 数字三角形
> 给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
>
> ```
> 7
> 3 8
> 8 1 0
> 2 7 4 4
> 4 5 2 6 5
> ```
>
> **输入格式**
>
> 第一行包含整数 $n$ ,表示数字三角形的层数。
>
> 接下来 $n$ 行,每行包含若干整数,其中第 $i$ 行表示数字三角形第 $i$ 层包含的整数。
>
> **输出格式**
>
> 输出一个整数,表示最大的路径数字和。
>
> **数据范围**
>
> $1≤n≤500$
> $−10000≤三角形中的整数≤10000$
>
> **输入样例**
>
> ```
> 5
> 7
> 3 8
> 8 1 0
> 2 7 4 4
> 4 5 2 6 5
> ```
>
> **输出样例**
>
> ```
> 30
> ```
```java
public class DigitalTriangle {
final static int N = 510;
final static int[][] f = new int[N][N];
public static void main(String[] args) throws IOException {
int n = Tools.getInt();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
f[i][j] = Tools.getInt();
for (int i = n - 1; i >= 1; i--)
for (int j = 1; j <= i; j++)
f[i][j] += Math.max(f[i + 1][j], f[i + 1][j + 1]);
System.out.print(f[1][1]);
}
}
```
### 最长公共前缀
> 给定两个长度分别为 $N$ 和 $M$ 的字符串 $A$ 和 $B$ ,求既是 $A$ 的子序列又是 $B$ 的子序列的字符串长度最长是多少。
>
> **输入格式**
>
> 第一行包含两个整数 $N$ 和 $M$ 。
>
> 第二行包含一个长度为 $N$ 的字符串,表示字符串 $A$ 。
>
> 第三行包含一个长度为 $M$ 的字符串,表示字符串 $B$ 。
>
> 字符串均由小写字母构成。
>
> **输出格式**
>
> 输出一个整数,表示最大长度。
>
> **数据范围**
>
> $1≤N,M≤1000$
>
> **输入样例**
>
> ```
> 4 5
> acbd
> abedc
> ```
>
> **输出样例**
>
> ```
> 3
> ```
```java
public class LongestCommonSubsequence {
final static int N = 10010;
final static char[] a = new char[N], b = new char[N];
final static int[][] f = new int[N][N];
final static StreamTokenizer cin = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
private static String getString() throws IOException {
cin.nextToken();
return cin.sval;
}
public static void main(String[] args) throws IOException {
int n = Tools.getInt(), m = Tools.getInt();
char[] s1 = getString().toCharArray();
System.arraycopy(s1, 0, a, 1, s1.length);
char[] s2 = getString().toCharArray();
System.arraycopy(s2, 0, b, 1, s2.length);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 取公共前缀的最大值
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j]) f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
}
}
System.out.println(f[n][m]);
}
```
### 最长上升子序列
> 给定一个长度为 $N$ 的数列,求数值严格单调递增的子序列的长度最长是多少。
>
> **输入格式**
> 第一行包含整数 $N$ 。
>
> 第二行包含 $N$ 个整数,表示完整序列。
>
> **输出格式**
> 输出一个整数,表示最大长度。
>
> **输入样例**
>
> ```
> 7
> 3 1 2 1 8 5 6
> ```
>
> **输出样例**
>
> ```
> 4
> ```
```java
public class LongestAscendingSubsequence {
final static int N = 100010;
final static int[] a = new int[N], f = new int[N];
public static void main(String[] args) throws IOException {
int n = Tools.getInt();
for (int i = 1; i <= n; i++) a[i] = Tools.getInt();
prime(n);
optimizen(n);
}
// O(N ^ 2)
private static void prime(int n) {
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] = Math.max(f[i], f[j] + 1);
}
}
int ret = 0;
for (int i = 1; i <= n; i++) ret = Math.max(ret, f[i]);
System.out.println(ret);
}
// O(N * logN)
private static void optimizen(int n) {
int len = 0;
f[0] = (int) 1e-9;
for (int i = 0; i < n; i++) {
int l = 0, r = len;
while (l < r) {
int mid = l + r + 1 >> 1;
if (f[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = Math.max(len, r + 1);
f[r + 1] = a[i];
}
System.out.println(len);
}
}
```
# 区间 DP
### 石子合并
> 设有N堆石子排成一排,其编号为 $1,2,3,…,N$。
>
> 每堆石子有一定的质量,可以用一个整数来描述,现在要将这 $N$ 堆石子合并成为一堆。
>
> 每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
>
> 例如有4堆石子分别为 `1 3 5 2`, 我们可以先合并 `1` 、`2` 堆,代价为 `4` ,得到 `4 5 2`, 又合并 `1` ,`2` 堆,代价为 `9` ,得到 `9 2` ,再合并得到 `11` ,总代价为 `4+9+11=24` ;
>
> 如果第二步是先合并 `2,3` 堆,则代价为 `7` ,得到 `4 7` ,最后一次合并代价为 `11` ,总代价为 `4+7+11=22` 。
>
> 问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
>
> **输入格式**
>
> 第一行一个数 $N$ 表示石子的堆数 $N$ 。
>
> 第二行 $N$ 个数,表示每堆石子的质量(均不超过1000)。
>
> **输出格式**
>
> 输出一个整数,表示最小代价。
>
> **数据范围**
>
> $1≤N≤300$
>
> **输入样例**
>
> ```
> 4
> 1 3 5 2
> ```
>
> **输出样例**
>
> ```
> 22
> ```
```java
public class MergeStones {
final static int N = 310;
final static int[] s = new int[N];
final static int[][] f = new int[N][N];
public static void main(String[] args) throws IOException {
int n = Tools.getInt();
for (int i = 1; i <= n; i++) s[i] = Tools.getInt();
Arrays.parallelPrefix(s, Integer::sum);
// 枚举长度
for (int len = 2; len <= n; len++) {
// 枚举左端点
for (int i = 1; i + len - 1 <= n; i++) {
int l = i, r = i + len - 1;
f[l][r] = (int) 1e8;
// 枚举分界点
for (int k = l; k < r; k++) {
f[l][r] = Math.min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
}
System.out.println(f[1][n]);
}
}
```
**参考**
- [算法基础课](https://www.acwing.com/activity/content/11/)

线性DP、区间DP