动态规划一

本文介绍了卡特兰数的定义及其递推公式,展示了如何用它解决二叉树形态计数问题和路径计数问题,以及0-1背包、完全背包、多重背包和分组背包的动态规划解决方案,包括线性DP在数字三角形和最长上升子序列中的应用,最后提及了Dilworth定理的相关内容。
摘要由CSDN通过智能技术生成

明安图数

又称卡特兰数(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)

应用

  1. 具有n个节点的二叉树的不同形态的种类

  2. 由1…n一共n个节点构成的不同的二叉搜索树题目连接

  3. 详细应用

  4. 从左上角走到右下角,只能向下或向右且不超过对角线![[Pasted image 20240228111004.png]]

  5. 唱票,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背包

  1. dp表示:f[i][j]表示只考虑前i个物品,且物品的总体积不超过j的方案的集合所得到的最大价值
  2. 核心代码:
/**
* 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
	}
}
  1. dp优化:根据状态转移方程,会发现每次更新f,都只会用到上一层的值,即f[i]的更新只由f[i-1]来更新,所以考虑使用一维数组,即滚动数组。
    ![[动态规划_202402202127_56165 3.jpg]]
  • 错误代码:
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]);	    
	}
}

完全背包

![[动态规划_202402202129_32846 4.jpg]]

朴素做法

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=s2k+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即可
![[纸_202402212106_12904 41.jpg]]

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 定理(狄尔斯沃定理)

  1. 最长不上升子序列的最小划分(个数 )= 最长上升子序列的长度
  2. 最长上升子序列的最小划分(个数)= 最长不上升子序列的长度
  • 31
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值