算法刷题记录-DP(LeetCode)
746. Min Cost Climbing Stairs
代码
int minCostClimbingStairs(vector<int>& cost) {if (cost.size()<2){return 0;}int cache[cost.size()+1];cache[0]=0;cache[1]=0;for (int i = 2; i <= cost.size(); ++i) {cache[i]= min(cache[i-2]+cost[i-2],cache[i-1]+cost[i-1]);}return cache[cost.size()];}
*873. Length of Longest Fibonacci Subsequence
思路
定义 f [ i ] [ j ] f[i][j] f[i][j]为使用 a r r [ i ] arr[i] arr[i] 为斐波那契数列的最后一位,使用 a r r [ j ] arr[j] arr[j] 为倒数第二位(即 a r r [ i ] arr[i] arr[i] 的前一位)时的最长数列长度。
不失一般性考虑 f [ i ] [ j ] f[i][j] f[i][j]该如何计算,首先根据斐波那契数列的定义,我们可以直接算得 a r r [ j ] arr[j] arr[j] 前一位的值为 a r r [ i ] − a r r [ j ] arr[i]−arr[j] arr[i]−arr[j],而快速得知 a r r [ i ] − a r r [ j ] arr[i]−arr[j] arr[i]−arr[j]值的坐标 t t t,可以利用 arr 的严格单调递增性质,使用「哈希表」对坐标进行转存,若坐标 t t t存在,并且符合 t < j t<j t<j,说明此时至少凑成了长度为 333 的斐波那契数列,同时结合状态定义,可以使用 f [ j ] [ t ] f[j][t] f[j][t]来更新 f [ i ] [ j ] f[i][j] f[i][j],即有状态转移方程:
f [ i ] [ j ] = m a x ( 3 , f [ j ] [ t ] + 1 ) f [ i ] [ j ] = max ( 3 , f [ j ] [ t ] + 1 ) f[i][j]=max(3,f[j][t]+1)f[i][j] = \max(3, f[j][t] + 1) f[i][j]=max(3,f[j][t]+1)f[i][j]=max(3,f[j][t]+1)
同时,当我们「从小到大」枚举 iii,并且「从大到小」枚举 j j j 时,我们可以进行如下的剪枝操作:
- 可行性剪枝:当出现 a r r [ i ] − a r r [ j ] > = a r r [ j ] arr[i]−arr[j]>=arr[j] arr[i]−arr[j]>=arr[j],说明即使存在值为 a r r [ i ] − a r r [ j ] arr[i]−arr[j] arr[i]−arr[j]的下标 t t t,根据 arr 单调递增性质,也不满足 t < j < i t<j<i t<j<i 的要求,且继续枚举更小的 j j j ,仍然有 a r r [ i ] − a r r [ j ] > = a r r [ j ] arr[i]−arr[j]>=arr[j] arr[i]−arr[j]>=arr[j],仍不合法,直接 break 掉当前枚举 j j j的搜索分支;
- 最优性剪枝:假设当前最大长度为 ans,只有当 j + 2 > a n s j+2>ans j+2>ans,我们才有必要往下搜索, j + 2 j+2 j+2 的含义为以 a r r [ j ] arr[j] arr[j] 为斐波那契数列倒数第二个数时的理论最大长度。
代码
public int lenLongestFibSubseq(int[] arr) {int ans=0;HashMap<Integer,Integer> finder=new HashMap<>();for (int i = 0; i < arr.length; i++) {finder.put(arr[i],i);}int[][] cache=new int[arr.length][arr.length];for (int i = 2; i < arr.length; i++) {for (int j = i-1; j >= 1; j--) {int residual=arr[i]-arr[j];if (residual>=arr[j]){break;}if (finder.containsKey(arr[i]-arr[j])){cache[i][j]=Math.max(3,cache[j][finder.get(residual)]+1);ans=Math.max(ans,cache[i][j]);}}}return ans;}
*877. Stone Game
数学解法
事实上,这还是一道很经典的博弈论问题,也是最简单的一类博弈论问题。
为了方便,我们称「石子序列」为石子在原排序中的编号,下标从 1
开始。
由于石子的堆数为偶数,且只能从两端取石子。因此先手后手所能选择的石子序列,完全取决于先手每一次决定。
证明如下:
由于石子的堆数为偶数,对于先手而言:每一次的决策局面,都能「自由地」
选择奇数还是偶数的序列,从而限制后手下一次「只能」
奇数还是偶数石子。
具体的,对于本题,由于石子堆数为偶数,因此先手的最开始局面必然是[奇数, 偶数]
,即必然是「奇偶性不同的局面」
;当先手决策完之后,交到给后手的要么是[奇数,奇数]
或者 [偶数,偶数]
,即必然是「奇偶性相同的局面」;后手决策完后,又恢复「奇偶性不同的局面」
交回到先手 …
不难归纳推理,这个边界是可以应用到每一个回合。
因此先手只需要在进行第一次操作前计算原序列中「奇数总和」和「偶数总和」哪个大,然后每一次决策都「限制」对方只能选择「最优奇偶性序列」的对立面即可。
同时又由于所有石子总和为奇数,堆数为偶数,即没有平局,所以先手必胜。
bool stoneGame(vector<int>& piles) {return true;}
动态规划
定义 f[l][r]
为考虑区间 [l,r]
,在双方都做最好选择的情况下,先手与后手的最大得分差值为多少。
那么 f[1][n]
为考虑所有石子,先手与后手的得分差值:
- f [ 1 ] [ n ] > 0 f[1][n]>0 f[1][n]>0,则先手必胜,返回 True
- f [ 1 ] [ n ] < 0 f[1][n]<0 f[1][n]<0,则先手必败,返回 False
不失一般性的考虑 f [ l ] [ r ] f[l][r] f[l][r]如何转移。根据题意,只能从两端取石子(令 piles
下标从 1
开始),共两种情况:
- 从左端取石子,价值为
piles[l - 1]
;取完石子后,原来的后手变为先手,从[l+1,r]
区间做最优决策,所得价值为f[l+1][r]
。因此本次先手从左端点取石子的话,双方差值为:
p i l e s [ l − 1 ] − f [ l + 1 ] [ r ] piles[l−1]−f[l+1][r] piles[l−1]−f[l+1][r] - 从右端取石子,价值为
piles[r−1]
;取完石子后,原来的后手变为先手,从[l,r−1]
区间做最优决策,所得价值为f[l][r−1]
。因此本次先手从右端点取石子的话,双方差值为:
p i l e s [ r − 1 ] − f [ l ] [ r − 1 ] piles[r−1]−f[l][r−1] piles[r−1]−f[l][r−1]
双方都想赢,都会做最优决策(即使自己与对方分差最大)。因此 f [ l ] [ r ] f[l][r] f[l][r] 为上述两种情况中的最大值。
根据动态规划的状态转移方程,计算 dp [ i ] [ j ] \textit{dp}[i][j] dp[i][j] 需要使用 dp[i+1][j]
和 dp[i][j−1]
的值,即区间 [i+1,j]
和 [i,j−1]
的状态值需要在区间[i,j]
的状态值之前计算,因此计算 dp[i][j]
的顺序可以是以下两种。
从小到大遍历每个区间长度,对于每个区间长度依次计算每个区间的状态值。
从大到小遍历每个区间开始下标 i i i,对于每个区间开始下标 i i i 从小到大遍历每个区间结束下标 jjj,依次计算每个区间 [i, j]
的状态值。
计算得到 dp[0][n−1]
即为 Alice 与 Bob 的石子数量之差最大值。如果 d p [ 0 ] [ n − 1 ] > 0 dp[0][n−1]>0 dp[0][n−1]>0,则 Alice 赢得游戏,返回true,否则 Bob 赢得游戏,返回 false。
class Solution {public boolean stoneGame(int[] ps) {int n = ps.length;int[][] f = new int[n + 2][n + 2]; for (int len = 1; len <= n; len++) { // 枚举区间长度for (int l = 1; l + len - 1 <= n; l++) { // 枚举左端点int r = l + len - 1; // 计算右端点int a = ps[l - 1] - f[l + 1][r];int b = ps[r - 1] - f[l][r - 1];f[l][r] = Math.max(a, b);}}return f[1][n] > 0;}
}
*915. Partition Array into Disjoint Intervals
思路
根据题意,我们知道本质是求分割点,使得分割点的「左边的子数组的最大值」小于等于「右边的子数组的最小值」。
我们可以先通过一次遍历(从后往前)统计出所有后缀的最小值 min,其中 min[i] = x 含义为下标范围在
[ i , n − 1 ] [i,n−1] [i,n−1] 的 n u m s [ i ] nums[i] nums[i]的最小值为 x,然后再通过第二次遍历(从前往后)统计每个前缀的最大值(使用单变量进行维护),找到第一个符合条件的分割点即是答案。
代码
public int partitionDisjoint(int[] nums) {int[] min=new int[nums.length];int[] max=new int[nums.length];min[nums.length-1]=nums[nums.length-1];for (int i = nums.length-2; i >=0 ; i--) {min[i]=Math.min(min[i+1],nums[i]);}max[0]=nums[0];for (int i = 1; i < nums.length; i++) {max[i]=Math.max(nums[i],max[i-1]);}int ans=0;for (int i = nums.length-2; i >=0; i--) {if (max[i]<=min[i+1]){ans=i;}}return ans+1;}
926. Flip String to Monotone Increasing
思路
根据题意可知,字符有0和1两种状态,所以我们维护一个二维的cache数组来记录每个字符的状况。
cache[i][0]表示第i个字符是0的变换次数,cache[i][1]表示第i个字符是1的变换次数。
根据单调性: 若 s [ i − 1 ] = = 0 s[i-1] == 0 s[i−1]==0,s[i]是0或者1都可以保持单调性。
若 s [ i − 1 ] = = 1 s[i-1] == 1 s[i−1]==1,s[i]则必须为1才可以保持单调性(必须满足i-1是1)。
所以
cache[i][0] = cache[i-1][0] + (s[i] == '1' ? 1 : 0);//(自己是0,则前边都是0)
cache[i][1] = Math.min(cache[i-1][0],cache[i-1][1]) + (s[i] == '0' ? 1 : 0);//(自己是1,前边0或者1都可以)
最后result = Math.min(cache[i][0],cache[i][1])
代码
public int minFlipsMonoIncr(String s) {int cache[][]=new int[s.length()][2];char[] arr=s.toCharArray();cache[0][0]=arr[0]=='0'?0:1;cache[0][1]=1-cache[0][0];for (int i = 1; i < arr.length; i++) {cache[i][0]=cache[i-1][0]+(arr[i]=='0'?0:1);cache[i][1]=Math.min(cache[i-1][0],cache[i-1][1])+(arr[i]=='1'?0:1);}return Math.min(cache[s.length()-1][0],cache[s.length()-1][1]);}