算法刷题记录-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 tt<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 tt<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[l1]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[r1]f[l][r1]

双方都想赢,都会做最优决策(即使自己与对方分差最大)。因此 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][n1]>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,n1] 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[i1]==0,s[i]是0或者1都可以保持单调性。
s [ i − 1 ] = = 1 s[i-1] == 1 s[i1]==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]);
    }

你可能感兴趣的:(算法,算法,leetcode,职场和发展)