labuladong/fucking-algorithm
求最值
问题最优子结构
,问题可以划分为子问题
,通过求解子问题,得到更大规模问题的解重叠子问题
,可以使用额外空间
进行优化记录解空间
(所有可能解),观察所有解之间的联系定义子问题
子问题之间的关系
,即是否能从1个子问题的解得到另一个子问题的解状态
子问题
,或定义dp[i]
的含义状态转移方程
联系
或dp[i]的推导关系
dp[0]
或dp[1]等…509. 斐波那契数
//时间O(n),空间O(1)
public int fib(int N) {
if(N<=0) return 0;
if(N == 1 || N == 2) return 1;
int first = 1;
int second = 1;
for(int i = 3 ; i <= N ;i++){
int num = first + second;
first = second;
second = num;
}
return second;
}
322. 零钱兑换
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
dp[0] = 0 ;
for(int i = 1 ; i <= amount ;i++){
int min = -1;//上一步拿硬币需要的最少次数
for(int coin : coins){
if(i-coin == 0){
//如果金额为i,其中有一个硬币为i
min = 0 ; //min = dp[0]
break;
}else if(i-coin>0 && dp[i-coin] != -1){
//如果还需要拿i-coin这么多金额,并且coins能组成这个金额
//那么更新min,如果min为-1,那么更新为dp[i-coin],否则保留最小值
min = min==-1?dp[i-coin]:Math.min(min,dp[i-coin]);
}
}
dp[i] = min==-1?-1:min+1;
}
return dp[amount];
}
例:
输入:N = 3, W = 4,wt = [2, 1, 3],val = [4, 2, 3]
输出:6,取前两个物品,重量为2+1=3,小于W,并且价值最大
当前可选择的物品
和背包容量
。状态
,定义dp[i][w]
表示在背包容量为w的情况下,从前i个物品中选出的物品最大价值,dp[n][W]
即为原问题的解选择
确定状态转移,对于第i个物品,要么选,要么不选,根据这两种选择的结果,从中取最大值即可dp[i][w]
= dp[i-1][w]
,dp[i][w]
= val[i-1]+dp[i-1][w-wt[i-1]]
,i
是从1开始,因此索引为i-1
,val[i-1]
表示当前物品的价值,dp[i-1][w-wt[i-1]
表示去除当前物品重量下的前i-1个物品的最大价值。dp[...][0] = 0
容量为0,不管怎么选,价值都为0dp[0][...] = 0
,没有物品可以选,价值当然为0int knapsack(int W, int N, int[] wt, int[] val) {
// vector 全填入 0,base case 已初始化
int[][] dp = new int[N+1][W];
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
494. 目标和
前i个数
和目标数
dp[n-1][S]
即为所求s+nums[i]
或s-nums[i]
,因此到达当前数字(节点)就有dp[i-1][s+nums[i]]+dp[i-1][s-nums[i]]
这么多方法(路径)2*sum+1
sum+S
public int findTargetSumWays(int[] nums, int s) {
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
// 绝对值范围超过了sum的绝对值范围则无法得到
if (Math.abs(s) > Math.abs(sum)) return 0;
int len = nums.length;
// - 0 +
int t = sum * 2 + 1;
int[][] dp = new int[len][t];
// 初始化
if (nums[0] == 0) {
dp[0][sum] = 2;//第一个数字为0,目标数为0,那么有2中方法+0或-0
} else {
dp[0][sum + nums[0]] = 1;//第一个数字不为0,目标数为nums[0]有1种方法
dp[0][sum - nums[0]] = 1;//第一个数字不为0,目标数为-nums[0]有1种方法
}
for (int i = 1; i < len; i++) {
for (int j = 0; j < t; j++) {
// 边界
int l = (j - nums[i]) >= 0 ? j - nums[i] : 0;
int r = (j + nums[i]) < t ? j + nums[i] : 0;
dp[i][j] = dp[i - 1][l] + dp[i - 1][r];
}
}
return dp[len - 1][sum + s];
}
300. 最长上升子序列
以nums[i]结尾的最长上升子序列(包括nums[i])长度
,那么原问题的解为max(dp[i])在前面找到比nums[i]小的元素,然后将nums[i]接到后面,就构成了一个新的最长上升序列,dp[i]就取其中的最大值+1
dp[i] = max(dp[j])
其中j属于nums[i]之前的元素中,满足nums[i]>nums[j]的索引public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
for(int i = 1 ; i < nums.length ;i++){
for(int j = 0 ; j < i ;j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
}
int max = 0;
for(int i = 0 ; i < nums.length ;i++){
max = Math.max(max,dp[i]);
}
return max;
}
72. 编辑距离
dp[i][j]
表示word1的前i个字符与word2的前j个字符的编辑距离,那么dp[m][n]
即为原问题的解dp[i][j] = dp[i-1][j]+1
,因为word1的前i-1个字符最少经过dp[i-1][j]次操作后,变为了word2的前j个字符,因此只需要在通过1次插入操作即可dp[i][j] = dp[i][j-1]+1
dp[i][j] = dp[i-1][j-1]+1
,因为word1的前i-1个字符最少经过dp[i-1][j-1]
次操作后,变为了word2的前j-1个字符,那么再需要1次替换即可,特别情况下,如果这两个字符相同,就不需要替换,即dp[i][j] = dp[i-1][j-1]
dp[0][j] = j , dp[i][0] = i
.因为空字符串到一个非空字符串的编辑距离肯定为非空字符串的长度public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 0 ; i < m+1 ;i++){
dp[i][0] = i;
}
for(int j = 0 ; j < n+1 ;j++){
dp[0][j] = j;
}
for(int i = 1 ; i < m+1 ;i++){
for(int j = 1 ; j < n+1 ; j++){
int insertOp = dp[i-1][j]+1;
int deleteOp = dp[i][j-1]+1;
int replaceOp = dp[i-1][j-1]+1;
if(word1.charAt(i-1) == word2.charAt(j-1)){
replaceOp--;
}
dp[i][j] = Math.min(insertOp,Math.min(deleteOp,replaceOp));
}
}
return dp[m][n];
}
887. 鸡蛋掉落
待完善
public int superEggDrop(int K, int N) {
//递归结束
if(K == 1) return N;
if(N == 0) return 0;
int result = Integer.MAX_VALUE;
//穷举所有的可能性
//从N层楼的第i层开始扔
for(int i = 1 ; i < N+1 ; i++){
//碎了,鸡蛋-1,从下面继续尝试
int broken = superEggDrop(K-1,i-1);
//没碎,鸡蛋不变,继续从上面尝试
int notBroken = superEggDrop(K,N-i);
//这两种情况要取最大值,才能反映最坏情况
//然后加上从i处扔的这一次,那么从i处扔至少需要的次数为
int max = Math.max(broken,notBroken)+1;
//更新result,取这么多次的最小值
result = Math.min(max,result);
}
return result;
}
416. 分割等和子集
sum/2
,这样可以看成一个01背包问题:dp[i][j]
表示在背包容量为j的情况下,从前i个物品中选,是否能够从中选取若干个物品,使其元素和等于j。那么原问题的解为dp[n][sum/2]
;nums[i-1]
来说,我们可以选也可以不选,因此遍历所有的选择即可dp[i][j] = dp[i-1][j-nums[i-1]];
选择当前元素,那么True还是False就依赖于背包容量减少后的前一个状态dp[i][j] = dp[i-1][j];
不选的话,背包容量不变,直接依赖于前一个状态dp[0][...] = False ;
//没有物品可以选了,显然为falsedp[...][0] = True ;
//背包容量为0就是装满了,为truepublic boolean canPartition(int[] nums) {
int n= nums.length;
int sum = 0 ;
for(int num : nums){
sum += num;
}
//如果sum为奇数,那么不可能
if((sum & 1) == 1){
return false;
}
boolean[][] dp = new boolean[n+1][sum/2+1];
for(int i = 0 ; i < n+1 ; i++){
dp[i][0] = true;
}
for(int j = 0 ; j < sum/2+1 ; j++){
dp[0][j] = false;
}
for(int i = 1 ; i < n+1 ;i++){
for(int j = 1; j < sum/2+1 ;j++){
//如果容量j已经不容装下nums[i-1],那就只能不装
if(j - nums[i-1] < 0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j-nums[i-1]]||dp[i-1][j];
}
}
}
return dp[n][sum/2];
}
518. 零钱兑换 II
//动态规划
//确定状态:可选择的硬币和总金额
//确定选择:选或不选
//dp定义:dp[i][j]表示前i个硬币组成总金额为j的方法数,原问题的解为dp[N][amout];
//状态转移:对于第i个硬币,可以选也可以不选
//选:dp[i][j] = dp[i][j-coins[i-1]] ; 例:前2个硬币组成金额为3的方法数为2,第三个硬币金额为4,选了,那么前3个硬币组成金额为7的方法数还是为2,(在之前每条路径上再添加第三枚硬币的金额,但还是那么多路径)
//不选:dp[i][j] = dp[i-1][j]; 例:前2个硬币组成金额为3的方法数为2,第三个硬币不选,那么前3个硬币组成金额为3的方法数还是为2
//初始化:
//dp[0][...] = 0 如果没有硬币,那么没办法凑成指定金额
//dp[...][0] = 1 总金额为0,什么都不拿就行了,也是1种方法
public int change(int amount, int[] coins) {
int[][] dp = new int[coins.length+1][amount+1];
for(int j = 0 ; j < amount+1 ; j++){
dp[0][j] = 0;
}
for(int i = 0 ; i < coins.length+1 ; i++){
dp[i][0] = 1;
}
for(int i = 1 ; i < coins.length+1 ;i++){
for(int j = 1; j < amount+1 ; j++){
if(j-coins[i-1]<0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
}
}
}
return dp[coins.length][amount];
}
1143. 最长公共子序列
//动态规划,画表、举例子即可解决
//状态: text1和text2的前i,j个字符 决定了 LCS
//选择 : i,j指向的字符是否一致
//定义dp:dp[i][j]表示text1,text2的前i,j个字符的LCS,那么原问题的解为dp[m][n]
//状态转移:
//text1[i-1]与text2[j-1]相等:dp[i][j] = dp[i-1][j-1]+1。相等的话,LCS加1
//不相等:dp[i][j] = max(dp[i][j-1],dp[i-1][j])。不相等的话,LCS取决于前面的状态
//如abc和ace,i=3,j=3时,c和e不相等,那么此时的LCS取决于(abc和ac的LCS),(ab,ace的LCS),从中取最大值
//初始化
//dp[0][...] = 0 , dp[...][0]=0
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 0 ; i < m+1 ; i++){
dp[i][0] = 0;
}
for(int j = 0 ; j < n+1 ; j++){
dp[0][j] = 0;
}
for(int i = 1 ; i < m+1 ; i++){
for(int j = 1 ; j < n+1 ;j++){
if(text1.charAt(i-1) == text2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
}
}
}
return dp[m][n];
}