区间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
答案:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str = bufferedReader.readLine().split(" ");
int n = Integer.parseInt(str[0]);
int[] a = new int[n + 1];
int[] s = new int[n + 1];
int[][] f = new int[n + 1][n + 1];
//f[i][j]=x表示所有将第i堆石子到第j堆石子合并成一堆石子的合并方式的最小代价值
str = bufferedReader.readLine().split(" ");
s[0] = 0;
for (int i = 1; i <= n; i++) {
a[i] = Integer.parseInt(str[i - 1]);
s[i] = s[i - 1] + a[i];
}
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]);
}
}
计数类DP
例题:整数划分
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n 的一种划分。
现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。
输入格式
共一行,包含一个整数 n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 109+7 取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
答案:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str = bufferedReader.readLine().split(" ");
int n = Integer.parseInt(str[0]);
int[] f = new int[n + 1];
f[0] = 1;
//同完全背包问题
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
f[j] = (f[j] + f[j - i]) % ((int) (1e9 + 7));
System.out.println(f[n]);
}
}
状态压缩DP
例题:蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
答案:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
static int N = 12, M = 1 << N; //M:2的N次方
static int n, m;
static long[][] f; // i表示列, j表示所有可能的状态
static boolean[] st; //是否奇数个0
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String[] str = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str[0]);
m = Integer.parseInt(str[1]);
if (n == 0 && m == 0)
break;
f = new long[N][M];
st = new boolean[M];
List<Integer>[] list = new ArrayList[M];
//横向摆放确定后纵向摆放只有一种即横向摆放的方案数(合法即可以放进去纵向的)等于总方案数
//状态:二进制
//状态表示:f[i,j]表示已经将前i-1列摆好,且从第i-1列,伸出到第i列的状态是j的所有方案
//预处理 得st数组
//对于每种状态,判断是否有奇数个连续的0
for (int i = 0; i < 1 << n; i++) {
int cnt = 0; //连续0的个数
boolean flag = true;
for (int j = 0; j < n; j++) {
if ((i >> j & 1) > 0) { //i>>j:代表i的第j位 >0代表第j位不是0
if ((cnt & 1) > 0) {
flag = false; //(cnt&1)>0代表cnt是奇数,该方案不合法,令flag为false
break;
}
cnt = 0;
} else cnt++;
}
if ((cnt & 1) > 0)
flag = false;
st[i] = flag;
}
//预处理 得List<Integer>[] list(合法方案)
for (int i = 0; i < 1 << n; i++) { //第i列所有状态
list[i] = new ArrayList<>();
for (int j = 0; j < 1 << n; j++) { //第i-1列所有状态
//(i&j)==0:i-1列伸到i的小方格 和i列放置的小方格不重复
//st[i|j]:所有连续空着的位置的长度必须是偶数
if ((i & j) == 0 && st[i | j]) {
list[i].add(j);
}
}
}
//dp
for (int i = 0; i < N; i++)
Arrays.fill(f[i], 0);
f[0][0] = 1;
for (int i = 1; i <= m; i++) {
for (int j = 0; j < 1 << n; j++) {
for (int k : list[j]) {
f[i][j] += f[i - 1][k];
}
}
}
System.out.println(f[m][0]);
}
}
}
树形DP
例题:没有上司的舞会
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入格式
第一行一个整数 N。
接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。
接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。
输出格式
输出最大的快乐指数。
数据范围
1≤N≤6000,
−128≤Hi≤127
输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出样例:
5
答案:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
static int N = 12010;
static int idx, n;
static int[] e, ne, h, happy;
static int[][] f;
static boolean[] fa;
public static void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
public static void dfs(int u) {
f[u][1] = happy[u]; //1:邀请 0:不邀请
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
dfs(j);
f[u][0] += Math.max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str[0]);
e = new int[N];
ne = new int[N];
h = new int[N];
happy = new int[n + 1];
f = new int[n + 1][2];
fa = new boolean[n + 1];
Arrays.fill(h, -1);
for (int i = 1; i <= n; i++) {
str = bufferedReader.readLine().split(" ");
happy[i] = Integer.parseInt(str[0]);
}
for (int i = 1; i < n; i++) {
str = bufferedReader.readLine().split(" ");
int l = Integer.parseInt(str[0]);
int k = Integer.parseInt(str[1]);
add(k, l);
fa[l] = true;
}
int root = 1;
while (fa[root]) root++;
dfs(root);
System.out.println(Math.max(f[root][0], f[root][1]));
}
}
记忆化搜索
例题:滑雪
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。
在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
输入格式
第一行包含两个整数 R 和 C。
接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。
输出格式
输出一个整数,表示可完成的最长滑雪长度。
数据范围
1≤R,C≤300,
0≤矩阵中整数≤10000
输入样例:
5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
输出样例:
25
答案:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
public class Main {
static int m, n;
static int[][] h;
static int[][] f;
static int[] dx = {0, -1, 0, 1};
static int[] dy = {-1, 0, 1, 0};
public static int dp(int x, int y) {
if (f[x][y] != -1)
return f[x][y]; //f[x][y]已经计算过则直接返回
f[x][y] = 1; //初始化为1
for (int i = 0; i < 4; i++) {
int a = x + dx[i];
int b = y + dy[i];
if (a >= 1 && a <= m && b >= 1 && b <= n && h[a][b] < h[x][y]) {
f[x][y] = Math.max(f[x][y], dp(a, b) + 1); //符合条件则取最长的滑雪长度
}
}
return f[x][y];
}
public static void main(String[] args) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str = bufferedReader.readLine().split(" ");
m = Integer.parseInt(str[0]);
n = Integer.parseInt(str[1]);
h = new int[m + 1][n + 1];
f = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
str = bufferedReader.readLine().split(" ");
for (int j = 1; j <= n; j++) {
h[i][j] = Integer.parseInt(str[j - 1]);
}
}
for (int i = 0; i <= m; i++)
Arrays.fill(f[i], -1);
int max = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
max = Math.max(max, dp(i, j));
}
}
System.out.println(max);
}
}