明安图数
又称卡特兰数(Catalan number),其前几项为(从第0项开始):1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190,…
递推公式:f[n] = f[0]*f[n-1]+f[1]*f[n-2]+f[2]*f[n-3]+...+f[n-2]*f[1]+f[n-1]*f[0]
即f[n] += f[k] * f[n - 1 - k]
(0 <= k <= n - 1)
应用
-
具有n个节点的二叉树的不同形态的种类
-
由1…n一共n个节点构成的不同的二叉搜索树题目连接
-
从左上角走到右下角,只能向下或向右且不超过对角线
-
唱票,A从未落后于B,但最后A和B的票数持平,问唱票方式种类数。模型就类似与走格子,向下走就相当于A的票,向右走就相当于B的票。
代码实现:
import java.util.*;
public class Catalan {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
long[] catalan = new long[n + 1];
catalan[0] = 1;
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= i - 1;j ++){
catalan[i] += catalan[j] * catalan[i - 1 - j];
}
}
System.out.println(Arrays.toString(catalan));
/**
* c[0] = 1
* c[n + 1] = 2(2n + 1)/(n + 2) * c[n]
*/
long c = 1;
for(long i = 0;i < n;i ++){
System.out.print(c + " ");
c = c * 2 * (2 * i + 1) / (i + 2);
}
}
}
背包问题
0-1背包
- dp表示:
f[i][j]
表示只考虑前i
个物品,且物品的总体积不超过j
的方案的集合所得到的最大价值 - 核心代码:
/**
* v[i] 表示第i个物品的体积
* w[i] 表示第i个物品的价值(权重)
*/
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= m;j ++){
f[i][j] = f[i - 1][j];
if(v[i] <= j)f[i][j] = Math.max(f[i][j],f[i - 1][j - v[i]] + w[i]);
// 不选择物品i 选择物品i
}
}
- dp优化:根据状态转移方程,会发现每次更新f,都只会用到上一层的值,即
f[i]
的更新只由f[i-1]
来更新,所以考虑使用一维数组,即滚动数组。
- 错误代码:
for(int i = 1;i <= n;i ++){
for(int j = v[i];j <= m;j ++){
f[j] = Math.max(f[j],[j - v[i]] + w[i]);
}
}
- 由与j是从小到大更新的,所以在计算当前层的
f[j]
时f[j - v[i]]
就已经被更新过,也即此时的f[j - v[i]]
不再时上一层的数据,所以会出现错误,所以我们只需要j从大到小即可,保证f[j - v[i]]
在f[j]
之后更新。 - 正确代码:
for(int i = 1;i <= n;i ++){
for(int j = m;j >= v[i];j --){
f[j] = Math.max(f[j],[j - v[i]] + w[i]);
}
}
完全背包
朴素做法:
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= m;j ++){
for(int k = 0;k * v[i] <= j;k ++){
f[i][j] = Math.max(f[i][j],f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
优化做法:
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= m;j ++){
f[i][j] = f[i - 1][j];
if(v[i] <= j)f[i][j] = Math.max(f[i][j],f[i][j - v[i]] + w[i]);
}
}
滚动数组:
for(int i = 1;i <= n;i ++){
for(int j = v[i];j <= m;j ++){
f[j] = Math.max(f[j],f[j - v[i]] + w[i]);
}
}
多重背包
朴素做法:类似于完全背包,区别在于完全背包的物品数量是无限的,但是多重背包却是有限制的,所以只需要修改完全背包中k的范围即可:
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] = Math.max(f[i][j],f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
二进制优化:
思路就是使用二进制把每一个物品的物品数拆分成若干组,使得其能够完成每一种方案的组合。假设一物品有7个,那么可以将他拆分成成四组每组的数量分别是[0,1,2,4],那么这个组就可以表示0~7的所有数,也就是说讲每一个组看成一个新的物品,其体积就是数量*原体积,其权重就是数量*原权重。
假设一个物品的是200,那么它的拆分可以是:[1,2,4,8,16,32,64,73],如果取128,那么就会出现200以上的数量,这是不符合题目要求的,73是200-[1,2,4,8,16,32,64]的最大组合。
一个物品的数量是s,那么它的组合是:1,2,4,…,2^k,c其中
c
=
s
−
2
k
+
1
c= s - 2^{k+1}
c=s−2k+1。这样就变成了0-1背包,只是把原始的物品有分成了只能选择一次的新物品,时间复杂度就很好的降低了。
代码实现:
import java.util.*;
public class Main{
public static void main(String[] args){
int N = 11010;
int M = 2010;
int[] v = new int[N];
int[] w = new int[N];
int[] f = new int[M];
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int cnt = 0;
for(int i = 1;i <= n;i ++){
int x,y,s;
x = in.nextInt();
y = in.nextInt();
s = in.nextInt();
int k = 1;
while(k <= s){
cnt ++;
v[cnt] = k * x;
w[cnt] = k * y;
s -= k;
k *= 2;
}
if(s > 0){
cnt ++;
v[cnt] = s * x;
w[cnt] = s * y;
}
}
n = cnt;
for(int i = 1;i <= cnt;i ++){
for(int j = m;j >= v[i];j --){
f[j] = Math.max(f[j],f[j - v[i]] + w[i]);
}
}
System.out.println(f[m]);
}
}
分组背包
分组背包与01背包大同小异,直接上使用滚动数组的代码:
import java.util.*;
public class Main{
public static void main(String[] ags){
Scanner in = new Scanner(System.in);
int N = 110;
int[][] v = new int[N][N];
int[][] w = new int[N][N];
int[] s = new int[N];
int[] f = new int[N];
int n = in.nextInt();
int m = in.nextInt();
for(int i = 1 ; i <= n ; i ++ ){
s[i] = in.nextInt();
for(int j = 1 ; j <= s[i] ; j ++ ){
v[i][j] = in.nextInt();
w[i][j] = in.nextInt();
}
}
for(int i = 1 ; i <= n ; i ++ ){
for(int j = m ; j >= 0 ; j -- ){
for(int k = 0; k <= s[i] ; k ++ ){
if(j >= v[i][k])
f[j] = Math.max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
System.out.println(f[m]);
}
}
线性DP
数字三角形
简单题,从上到下、从下到上都可以解决。
最长上升子序列
简单题,注意在初始化f时全为1即可
Arrays.fill(f,1);
int res = 1;
for(int i = 2;i <= n;i ++){
for(int j = 1;j < i;j ++){
if(w[i] > w[j])f[i] = Math.max(f[i],f[j] + 1);
}
res = Math.max(res,f[i]);
}
System.out.println(res);
最长上升子序列II
当数据量比较大时,用朴素的做法会TLE,这里可以使用另一种做法。我们维护一个数组q,q[i]表示长度为i的所有递增子序列中结尾的数值的最小值。例如:假设序列其长度为3的递增子序列分别123,124那么q[3]=3。
那么我们在遍历数组的时候,我们可以从q中找到比当前数(下标i)小的最大的数(下标r),我们这个数就可以插入到r+1位置,也就是说我们找到了长度是r+1的递增子序列,且结尾的数值比上一次的小,由于我们的序列是从左到右遍历的,所以不会存在遗漏的情况。
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] w = new int[n + 1];
int[] q = new int[n + 1];
for(int i = 1;i <= n;i ++){
w[i] = in.nextInt();
}
int len = 0;
for(int i = 1;i <= n;i ++){
int l = 0,r = len;
while(l < r){
int mid = l + r + 1 >> 1;// 剩余两个元素的时候,如果不+1,就会一直循环,因为mid一直取剩余的两个元素的第一个元素
if(q[mid] < w[i])l = mid;
else r = mid - 1;
}
len = Math.max(len,r + 1);
q[r + 1] = w[i];
}
System.out.println(len);
}
}
Dilworth 定理(狄尔斯沃定理)
- 最长不上升子序列的最小划分(个数 )= 最长上升子序列的长度
- 最长上升子序列的最小划分(个数)= 最长不上升子序列的长度