nums代表一排气球,nums[i]代表第i个气球的分数,现在要求戳破所有气球,请计算最多可能获得的分数i个气球的时候,可以获得nums[left]*nums[i]*nums[right]全排列序列下的得分,取最大值,一定可以得到答案做出选择后推进+撤销选择 private int res = Integer.MIN_VALUE;
public int maxCoins(int[] nums) {
ArrayList<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}//对数值赋值一份
backTrack(list,0);
return res;
}
private void backTrack(ArrayList<Integer> list, int score){
if(list.isEmpty()){
res = Math.max(res,score);
return;
}
for(int i = 0;i<list.size();i++){
int temp = list.get(i);//记忆起来,防止忘记
int tempCoin = list.get(i)*( i-1<0 ? 1: list.get(i-1)) * (i+1 >= list.size() ?1 : list.get(i+1));
//做选择
list.remove(i);//选择了第i个,然后就把第i个给删除掉
backTrack(list,score + tempCoin);
//撤销选择
list.add(i,temp);
}
}
nums[i],得到的分数和该气球相邻的气球nums[left]和nums[right]是有相关性的,而这种情况下就是子问题间产生了相互联系,不独立,彼此间有着耦合关系,因此我们必须巧妙地定义dp数组的含义,避免子问题产生相关性,才能写出状态转移方程dp[i][j]数组的定义,三步走dp[i][j]:代表着戳(i,j)区间所能得到的最大得分,注意这是开区间哈,不包含i和j两个断点,同时容易得到最终答案应该是dp[0][n+1]base-case:当0<=i<=n+1,j<=i+1的时候,dp[i][j] = 0状态转移方程推导
(i,j)区间里边的时候,想一想哪个气球会是最后被戳破的那一个选择来的i和j的值移动情况,选择就是选择哪个气球为k (i,k)、(k,j)里面的气球都给戳破,所以我们可以通过dp数组来算这个值先,然后戳破第k个气球的时候,会得分,由于k已经是最后一个气球了,又根据dp数组的定义,这时候它左右两边的气球是i和j,所以可以根据这个来计算得分穷举k,穷举在(i,j)之内的所有k,看谁能够得到本区间的最大值dp[i][j] = dp[i][k] + dp[k][j] + (points[i]*point[k]*point[j])
状态转移所依赖的状态必须被提前计算出来,思路也是有的,也就是根据base-case和最终状态进行推导[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5056N6gC-1659781449019)(./image/312-1.png)]
base-case分布在对角线上,而最终答案确定是在DP-Table的右上角,对于任意一个dp[i][j],我们希望所有dp[i][k]和dp[k][j]已经被计算[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpHmy3mV-1659781449020)(./image/312-2.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FY8ufYjv-1659781449021)(./image/312-3.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVFle9eM-1659781449022)(./image/312-4.png)]
class Solution {
public int maxCoins(int[] nums) {
//1.扩展数组
int n = nums.length;
int[] points = new int[n+2];
points[0]=points[n+1]=1;
for(int i = 1;i<points.length-1;i++){
points[i] = nums[i-1];
}
//2.定义dp数组,注意(i,j)是开区间,代表这个区间内的所能得到的最大得分,最小下标0,最大下标n+1
int[][] dp = new int[n+2][n+2];
//3.写出base-case,注意到dp的区间是(i,j),所以当j<=i+1的时候就压根就没有这个区间,那么也就压根没有最大值可以选了
//想一下(i,i+1)这个区间有值吗?至少得相差个2吧
for(int i =0;i<=n+1; i++){
for(int j = 0;i!=n+1&&j<=i+1;j++){
dp[i][j] = 0;
}
}
//4.状态转移推算,从下到上,从中间对角线到右上角,保证每一个值的下边和左边都被计算过了,因为n+1的那一行已经被计算过了(base-case),因此直接从n开始算
for(int i = n;i>=0;i--){
for(int j = i+1 ; j<n+2; j++){
//做选择,从(i,j)里面选一个k出来
for(int k = i+1;k<j;k++){
dp[i][j] = Math.max(dp[i][j],
dp[i][k]+dp[k][j]+(points[k]*points[i]*points[j]));
}
}
}
return dp[0][n+1];
}
}
W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],选择让你用这个背包来装物品,最多能装的价值是多少?直接开始三步走
明确状态和选择
对于每件物品而言,装进背包或者不装进背包明确dp数组的含义
dp[i][w]:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]base-case
确定状态转移方程:状态转移与选择息息相关
dp[i][w]应该要等于dp[i-1][w],因为不选这个物品,那么背包容量就还是w,继承之前的结果dp[i][w]=dp[i-1][w-wt[i-1]]+val[i-1]:如果装了第i个物品,就要寻求剩余重量w-wt[i-1]限制下的最大价值,再加上第i个物品的价值val[i-1]即可 public int backPack(int W,int N,int[] wt,int[] val){
//1.定义dp数组,dp[i][w]是指对于前i个物品,当前背包容量为w,这时候所能得到的最大价值
//要确定数组的大小,必须确定最终的答案:dp[N][W]
int[][] dp = new int[N+1][W+1];
//2.写出base-case,当i或者w有其中有一个等于0的时候,这个值就是0
dp[0][0] = 0 ;
for(int i = 1;i <= N;i++){
dp[i][0] = 0;
}
for(int w = 1;w <= W ; w++){
dp[0][w] = 0;
}
//3.写出状态转移方程
//3.1 确定遍历顺序:根据DP-table,容易知道,此时Dp-table被初始化为第0列为0,第0行为0的一个正方形表
//3.1 根据状态转移方程,容易知道,我们希望得到的是这个元素的左上角已经被全部计算完了,因此遍历顺序是顺序遍历的(可以画图验证)
for(int i = 1; i <= N;i++){
for(int j = 1; j <= W;j++){
//现在试图装第i-1个物品
if(j-wt[i-1]<0){//如果现在装不下,那么只能选择不装进背包
dp[i][j] = dp[i-1][j];
}else{//否则就是可以装下背包,有选择的余地
dp[i][j] = Math.max(
dp[i-1][j],//选择不装入背包,继承不装这个东西进背包,但容量保持这么多的
dp[i-1][j-wt[j]]+val[i-1]//选择装入背包
);
}
}
}
return dp[N][W];
}
nums,判断这个数组是否可以被分割成两个子集,使得两个子集的元素和相等W的背包和N个物品,每个物品有重量和价值两个属性,其中第i个物品的重量为wt[i],价值为val[i],现在用这个物品装物品,最多能装的价值是多少?sum,将问题转化为背包问题:dp[i][j]=x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包给装满,若x为false,则说明不能恰好将背包给装满,比如说dp[4][9]=true:对于容量为9的背包,若只使用前4个物品,则存在一种方法恰好将背包装满dp[N][sum/2],base-case就是当背包没有空间的时候,你都已经可以不用选了,因为这时候已经装满了,那么就是true,当没有物品可以选择的时候,如果这时候背包容量还不是空,那么就肯定装不满了状态转移dp[i-1][j],继承之前的结果dp[i-1][j-nums[i-1]],如果装了第i个物品,就要看背包的剩余重量j-nums[i-1]限制下是否能够恰好装满,换句话说错,如果j-nums[i-1]的重量可以被恰好装满,那么只要把第i个物品装进去,也可以恰好装满j的重量,否则肯定是不能够恰好装满重量j的 public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum+=num;
}
if(sum%2!=0){return false;}
int n = nums.length;
sum /=2;
boolean[][] dp = new boolean[n+1][sum+1];
for(int i =0;i<=n;i++){
dp[i][0] = true;
}
for(int i = 1;i<=n;i++){
for(int j = 1;j<=sum;j++){
if(j-nums[i-1]<0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
dp[i][j]都是通过上一行dp[i-1][...]转移过来的,所以可以压缩成一维的 public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for (int num : nums) {
sum+=num;
}
if(sum%2!=0){
return false;
}
sum/=2;
boolean[] dp = new boolean[sum+1];
dp[0] = true;
for(int i = 0;i<n;i++){
for(int j = sum;j>=0;j--){
if(j-nums[i]>=0){
dp[j] = dp[j] || dp[j-nums[i]];
}
}
}
return dp[sum];
}
给定不同面额的硬币coins和一个总金额amount,写一个函数来计算可以凑成总金额的硬币组合数,假设每一种面额的硬币有无限个
翻译成背包问题的说法:有一个背包,其最大容量是amount,有一系列的物品coins,每个物品的重量是coins[i],每个物品的数量无限,请问存在多少种方法能够把背包恰好给装满?
首先是动态规划三步走
第一明确两点,“状态"和"选择”
确定dp数组的含义:dp[i][j]可以用来表示若只使用前i个物品,当背包容量为j的时候,有dp[i][j]种方法可以装满背包,若只使用coins的前i个物品,当背包容量为j的时候,有dp[i][j]种凑法
写出base-case:当i=0的时候,也就是没有东西可以选了,这时候有0种凑法,当j=0的时候,表示当前背包容量为0,这时候有0种凑法
最终得到算法的输出是dp[N][amount]
确定状态转移方程
i个物品装入背包,也就是说不使用coins[i]这个面值的硬币,那么凑出面额j的方法数dp[i][j]应该要等于dp[i-1][j],继承之前的结果i个物品装入背包,那么dp[i][j]应该要等于dp[i][j-coins[i-1]]coins的索引是i-1时表示第i个硬币的面值dp[i][j]是选择和不选择两种选择所产生的凑法之和 public int change(int amount, int[] coins) {
//1.定义dp数组,dp[i][j]:表示的是选择前i个物品,在j的背包容量下,共有几种凑法
int[][] dp = new int[coins.length+1][amount+1];
//2.定义base-case
//当i=0的时候,表示没有硬币可以选
//当j=0的时候,表示背包没有容量,如果背包没有容量,那么这也是一种凑法,因为根据定义来说,我们都根本不用选硬币
dp[0][0]=0;
for(int i = 0;i<=coins.length;i++){
dp[i][0] = 1;
}
for(int j = 0;j<=amount;j++){
dp[0][j] = 0;
}
//3.开始状态转移计算,由base-case得到的DP-table可知从(1,1)开始
for(int i = 1;i<=coins.length;i++){
for(int j = 1;j<= amount; 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];
}
nums表示,每个元素nums[i]代表第i间房子中的现金数额,现在你可以从房子中取钱,但是有一个约束条件,相邻的房子的钱不能被同时取出(或者其他条件),你需要尽可能多地取出钱,请你编写一个算法,计算在满足调节的前提下最多能够取出多少钱? private int[] memo;
public int rob(int[] nums) {
memo = new int[nums.length+5];
Arrays.fill(memo,-1);
return dp(nums,0);//从0开始走过
}
/**
* dp函数的定义:表示对于一个数组nums,从start开始走,能够抢劫得到的最大金额是多少?
* @param nums
* @param start
* @return
*/
private int dp(int[] nums,int start){
//start代表的是索引,表示目前的状态,当start大于等于nums.length了,这时候不可能在抢劫了,是空区间,返回0值
if(start >= nums.length){//由于可能存在+2的情况,因此这里写>=
return 0;
}
//备忘录优化
if(memo[start] != -1){
return memo[start];
}
//择优,取max
int res = Math.max(
dp(nums,start+2)+nums[start],//选择抢这个屋子的
dp(nums,start+1)//选择不抢这个屋子的
);
memo[start] = res;
return res;
}
自底向上解法
我们可以根据dp()的原型,定义出dp数组,dp(int i)定义的是从i开始打劫,能够得到的最大金额是多少
那么dp[i]可以定义为,从第i间房子开始做选择,能够得到的最大金额是多少
遍历方向要根据base-case所定义的DP-table来决定,我们规定,当从第n间房子开始走的时候,这时候能取到的钱是0,那么也就是最后一个元素是0,那么我们遍历就要从倒数第二个元素来开始遍历
public int rob(int[] nums) {
int[] dp = new int[nums.length+2];
dp[nums.length] = 0;
for(int i = nums.length-1;i>=0;i--){
dp[i] = Math.max(
dp[i+1],//当累加上一间房子的最大金额,就代表着上一间房子被拿了,那么这里就不加
dp[i+2]+nums[i]//当累加上两间房子的最大金额,就代表是上两件房子被拿
);
}
return dp[0];
}
public int rob(int[] nums) {
int dp_i_1 = 0,dp_i_2=0,dp_i=0;
for(int i= nums.length-1;i>=0;i--){
dp_i = Math.max(dp_i_1,nums[i]+dp_i_2);
dp_i_2 = dp_i_1;
dp_i_1 = dp_i;
}
return dp_i;
}
private int helper(int[] nums,int start,int end){
int[] dp = new int[nums.length+2];
dp[end+1] = 0;
for(int i = end;i>=start;i--){
dp[i] = Math.max(
dp[i+2]+nums[i],
dp[i+1]
);
}
return dp[start];
}
public int rob(int[] nums) {
if(nums.length == 1){
return nums[0];
}
return Math.max(helper(nums,0,nums.length-2),helper(nums,1,nums.length-1));//是否选择拿第一间房子的钱
}
这道题是再升级版,要求获取钱的房子是不能有边的。
依旧是按照选择的套路来做,如果说我要拿这个房子的钱,那我要走下家的时候,只能走下家的下家
如果我不拿这个房子的钱,就可以走下家
private Map<TreeNode,Integer> memo = new HashMap<>();
public int rob(TreeNode root) {
if(root == null){
return 0;
}
if(memo.containsKey(root)){
return memo.get(root);
}
int do_it = root.val
+(root.left == null ?
0: rob(root.left.left)+rob(root.left.right))
+(root.right == null ?
0: rob(root.right.left) + rob(root.right.right));
int not_do = rob(root.left)+rob(root.right);
int res = Math.max(do_it,not_do);
memo.put(root,res);
return res;
}
nums和一个目标值target,现在你可以给每一个元素nums[i]添加+或者-,请计算出有几种符号的组合能够使得nums中的元素的和为target?function backtrack(路径,选择列表){
if 满足了结束条件{
result.add(路径)
return
}
for(选择 in 选择列表){
做选择
backtrack(路径,选择列表)
撤销选择
}
}
那么对于这道题来说,有以下几个关键点
根据上述分析可以先编写出代码
private int res = 0;
private int[] op = new int[]{1,-1};
/**
* 回溯算法:
* @param nums
* @param i:代表的是目前要对哪个数字进行加减的置换
* @param rest:代表的是距离target的程度,当为0的时候,意味着找到了一组可行的解决方案
*/
private void backtrack(int[] nums,int i,int rest){
if(i==nums.length){
if(rest == 0){
res ++ ;
}
return;
}
for(int j = 0;j< op.length;j++){
rest += op[j]*nums[i];
backtrack(nums,i+1,rest);
rest -= op[j]*nums[i];
}
}
public int findTargetSumWays(int[] nums, int target) {
backtrack(nums,0,target);
return res;
}
例如:
void backtrack(int i,int rest){
backtrack(i+1,rest-nums[i]);
backtrack(i+1,rest+nums[i]);
}
如果 nums[i]==0时,上述式框架就变成了
void backtrack(int i,int rest){
backtrack(i+1,rest);
backtrack(i+1,rest);
}
private int[] op = new int[]{1,-1};
private Map<String,Integer> memo;
/**
* 回溯算法:
* @param nums
* @param i:代表的是目前要对哪个数字进行加减的置换
* @param rest:代表的是距离target的程度,当为0的时候,意味着找到了一组可行的解决方案
*/
private int backtrack(int[] nums,int i,int rest){
if(i==nums.length){
if(rest == 0){
return 1;
}
return 0;
}
String key = i+","+rest;
if(memo.containsKey(key)){
return memo.get(key);
}
int sum = 0;
for(int j = 0;j< op.length;j++){
rest += op[j]*nums[i];
sum+= backtrack(nums,i+1,rest);
rest -= op[j]*nums[i];
}
memo.put(key,sum);
return sum;
}
A和B,分别代表着分配+的数和分配-的数,那么它们和target存在以下关系sum(A)-sum(B) = target;
sum(A) = target+sum(B);
sum(A)+sum(A) = target+sum(B)+sum(A);
2*sum(A)=target+sum(nums);
sum(A) =(target+sum(nums))/2;
sum,现在给你N个物品,第i个物品的重量为nums[i-1](注意1<=i<=N),每个物品只有一个,请问有几种不同的方法能够恰好装满这个包?目前背包的容量和可选择的物品是否将该物品加入背包dp[i][j] =x:表示若只在前i个物品中选择,而且当前背包的容量为j,则最多有x种方法可以恰好装满背包dp[i][j]=0dp[N][sum]nums[i]算入子集,或者说不把这第i个物品装入背包,那么恰好装满背包的方法数就取决于上一个状态dp[i-1][j],继承之前的结果nums[i]算入子集,那么只要看前i-1个物品有几种方法可以装满j-nums[i-1]的重量就可以了dp[i][j]对应的是所有能够装满背包的选择,所以应该要用加法。 private int helper(int[] nums,int target){//0-1背包问题
//1.定义dp数组,dp[i][j]:前i个物品和j的背包容量
int[][] dp = new int[nums.length+1][target+1];
//2.写出base-case
//2.1 当i等于0的时候,找不到一组合适的方法
for(int j=0;j<=target;j++){
dp[0][j] = 0;
}
//2.2 当没有数可以选而且背包容量又是0的时候,这时候才是0,书上写错了
dp[0][0] =1;
//3.确定遍历方向,由DP-table可知初始化的是第零行,因此从(1,0)开始遍历到后边即可
for(int i = 1;i<=nums.length;i++){
for(int j = 0;j<=target;j++){//注意,凑成0的选法有很多种,我们必须计算出个数,base-case是无法确定的
if(j-nums[i-1] <0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j]+dp[i-1][j-nums[i-1]];
}
}
}
return dp[nums.length][target];
}
public int findTargetSumWays(int[] nums, int target) {
//sum(A) =(target+sum(nums))/2;
int sum = 0;
for (int num : nums) {
sum+=num;
}
int sumADouble = target+sum;//如果是奇数,那么就无法找到一组合适的划分方法,直接返回0
//如果全部正数的num加起来都到不了target,我还要给它赋值负数呢,这时候怎么可能会有解?
if( sum < target || sumADouble%2==1 ){
return 0;
}
//注意有可能sum+target会小于0的,我们可以加上绝对值来规避这个问题
//为什么加上绝对值结果还不变?这是因为我们使用的是加减号这个符号来控制数值,如果减号成立,那么加号也肯定是成立的
return helper(nums,Math.abs((sum+target)/2));
}
}
return dp[nums.length][target];
}
public int findTargetSumWays(int[] nums, int target) {
//sum(A) =(target+sum(nums))/2;
int sum = 0;
for (int num : nums) {
sum+=num;
}
int sumADouble = target+sum;//如果是奇数,那么就无法找到一组合适的划分方法,直接返回0
//如果全部正数的num加起来都到不了target,我还要给它赋值负数呢,这时候怎么可能会有解?
if( sum < target || sumADouble%2==1 ){
return 0;
}
//注意有可能sum+target会小于0的,我们可以加上绝对值来规避这个问题
//为什么加上绝对值结果还不变?这是因为我们使用的是加减号这个符号来控制数值,如果减号成立,那么加号也肯定是成立的
return helper(nums,Math.abs((sum+target)/2));
}