第17周LeetCode记录

1.3 81. 分割等和子集

给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

思路

和除以2,判断数组中元素和是否可以组成。

最优解

状态定义:dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数,每个数只能用一次,使得这些数的和恰好等于 j

状态转移方程:

public class Solution {

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        // 题目已经说非空数组,可以不做非空判断
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        // 特判:如果是奇数,就不符合要求
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;
        // 创建二维状态数组,行:物品索引,列:容量(包括 0)
        boolean[][] dp = new boolean[len][target + 1];

        // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
        if (nums[0] <= target) {
            dp[0][nums[0]] = true;
        }
        // 再填表格后面几行
        for (int i = 1; i < len; i++) {
            for (int j = 0; j <= target; j++) {
                // 直接从上一行先把结果抄下来,然后再修正
                dp[i][j] = dp[i - 1][j];

                if (nums[i] == j) {
                    dp[i][j] = true;
                    continue;
                }
                if (nums[i] < j) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[len - 1][target];
    }
}

在「填表格」的时候,当前行只参考了上一行的值,因此状态数组可以只设置 22 行,使用「滚动数组」的技巧「填表格」即可;

实际上,在「滚动数组」的基础上还可以优化,在「填表格」的时候,当前行总是参考了它上面一行 「头顶上」 那个位置和「左上角」某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。

public class Solution {

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;
        boolean[] dp = new boolean[target + 1];
        dp[0] = true;

        if (nums[0] <= target) {
            dp[nums[0]] = true;
        }
        // 从第一行
        for (int i = 1; i < len; i++) {
            // 倒着画表
            for (int j = target; nums[i] <= j; j--) {
                if (dp[target]) {
                    return true;
                }
                dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

最优解总结

二维数组转为一维数组,利用了剪枝,相同的判断可以优化。此类背包问题要先画表,0-1背包问题(非对即错)

包的数量是元素和/2

1.4 82. 一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

最优解思路

0-1背包,dp[i][j]表示i个0,j个1所能拼成的最大子集的容量。画表可以推导出来。

画表方法,判断是否可以容纳,选定哪些背包有容量可以放。可以放的包可以调用状态转移方程。

最优解

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        if len(strs) == 0:
            return 0
        
        dp = [[0]*(n+1) for _ in range(m+1)]   #准备很多个背包
        
        for strs_item in strs:
            item_count0 = strs_item.count('0')
            item_count1 = strs_item.count('1')
            
            #遍历可容纳的背包 
            for i in range(m, item_count0 - 1, -1):  #采取倒序
                for j in range(n, item_count1 - 1, -1):
                    dp[i][j] = max(dp[i][j], 1 + dp[i-item_count0][j-item_count1])
                    
        return dp[m][n] 

总结

背包问题的特征有了新的认识,即每个背包都是一种可能,每个背包都有状态转移方程。

1.5 83. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

输入:coins = [2], amount = 3
输出:-1

最优解思路

动态规划

输入: coins = [1, 2, 5], amount = 11
凑成面值为 11 的最少硬币个数可以由以下三者的最小值得到:

凑成面值为 10 的最少硬币个数 + 面值为 1 的这一枚硬币;
凑成面值为 9 的最少硬币个数 + 面值为 2 的这一枚硬币;
凑成面值为 6 的最少硬币个数 + 面值为 5 的这一枚硬币。
即 dp[11] = min (dp[10] + 1, dp[9] + 1, dp[6] + 1)。

dp[amount] = min(dp[amount], 1 + dp[amount - coins[i]]) for i in [0, len - 1] if coins[i] <= amount

最优解

    public int coinChange(int[] coins, int amount) {
        // 给 0 占位
        int[] dp = new int[amount + 1];

        // 注意:因为要比较的是最小值,这个不可能的值就得赋值成为一个最大值
        Arrays.fill(dp, amount + 1);

        // 理解 dp[0] = 0 的合理性,单独一枚硬币如果能够凑出面值,符合最优子结构
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i - coin >= 0 && dp[i - coin] != amount + 1) {
                    dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
                }
            }
        }

        if (dp[amount] == amount + 1) {
            dp[amount] = -1;
        }
        return dp[amount];
    }

1.9 84. 两数相除

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。

返回被除数 dividend 除以除数 divisor 得到的商。

整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2

最优解

利用位运算求解

位移操作可以模拟乘法:

如:7 * 13 = 7 * (8 + 4 + 1) = 7 * 8 + 7 * 4 + 7 * 1

其中8,4,1分别是2的3,2,0次幂,因此

7 * 13 = 7 << 3 + 7 << 2 + 7 << 0 , 对于本题来说,即是:

a * x = b (暂不考虑余数)时,求x。根据上面位移运算模拟乘法的逻辑,可以把x看做一个由2的整数次幂的指数部分组成的一个数组。
如7 * 13,则13大约可以看成是[3,2,0]。因此本题可以转化为求此数组,之后累加2的数组元素次幂即可。

例子: 3 * x = 10

3 << 1 = 6 且 3 << 2 = 12
因12已经超过10的范围了,故y1 = 1。

然后,求 3 * x = (10 - (3 << 1)) 即3 * x = 4

可得y2 = 0,因为3 << 1 = 6,已经超过4了。

之后4 - (3 << 0) = 4 - 3 = 1。由于1已经小于3,不需要再继续找y3了。

故最终得出的y相关的数组为[1,0],则 x = 2^1 + 2^0 = 2 + 1 = 3(注:2^1 = 1<<1, 而在不溢出的情况下2 ^ n = 1 << n,后面不再赘述)。

最优解

var divide = function (dividend, divisor) {
    var INT_MAX = 0x7FFFFFFF;
    var INT_MIN = 1 << 31;

    //先判断符号
    var symbol = (dividend ^ divisor) >> 31;
    //由于Math.abs(INT_MIN)存在溢出问题
    //因此被除数与除数全部转为负数处理
    var _dividend = dividend > 0 ? -dividend : dividend;
    var _divisor = divisor > 0 ? -divisor : divisor;

    var times = divided_negtive(_dividend, _divisor);

    var output = 0;
    for (var i = 0; i < times.length; i++) {
        if (times[i] === 31) {
            //i=31表示INT_MIN,times无第二个元素,直接短路处理
            if (symbol === 0) {
                //符号为正,此时存在INT_MIN转为正数溢出,返回INT_MAX
                return INT_MAX;
            }
            return INT_MIN;
        }
        output += (1 << times[i]);
    }
    return symbol ? -output : output;

};


function divided_negtive(dividend, divisor) {
    //两负数相除
    //如-10/-20当除数小于被除数时,商为0
    if (divisor < dividend) {
        return [];
    }

    var timesMax = 32;
    var timesMin = 0;
    while (timesMax !== timesMin + 1) {
        //二分查找
        var mid = (timesMax + timesMin) >> 1;
        //divisor<<mid后有可能超过-1<<31的范围
        //因此要判断divisor是否大于等于-1<<(31-mid),一旦小于这个值,则必定溢出
        if (divisor < (-1 << (31 - mid))) {
            //符合溢出条件,说明mid过大,将mid赋给timesMax,供下次折半查找使用
            timesMax = mid;
            continue;
        }

        var testVal = divisor << mid;
        if (testVal < dividend) {
            timesMax = mid;
        } else {
            timesMin = mid;
        }
    }
    return [timesMin].concat(divided_negtive(dividend - (divisor << timesMin), divisor));
}

总结

  • 用二分法最快确定位移的位数
  • 异或右移31位取符号

1.12 85. 存在重复元素

在整数数组 nums 中,是否存在两个下标 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t ,且满足 i 和 j 的差的绝对值也小于等于 ķ 。

如果存在则返回 true,不存在返回 false。

输入: nums = [1,2,3,1], k = 3, t = 0
输出: true

输入: nums = [1,0,1,1], k = 1, t = 2
输出: true

我的解

滑动窗口的大小知道,从前到后遍历。

最优解

public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
    TreeSet<Integer> set = new TreeSet<>();
    for (int i = 0; i < nums.length; ++i) {
        // Find the successor of current element
        Integer s = set.ceiling(nums[i]);
        if (s != null && s <= nums[i] + t) return true;

        // Find the predecessor of current element
        Integer g = set.floor(nums[i]);
        if (g != null && nums[i] <= g + t) return true;

        set.add(nums[i]);
        if (set.size() > k) {
            set.remove(nums[i - k]);
        }
    }
    return false;
}

最优解总结

用set的大小保证了滑动窗口的大小,set.ceiling cet.floor可以获取前继节点和后继节点。java中的treeSet 二叉排序树(BST)

原文地址:https://www.cnblogs.com/jimmyhe/p/14289312.html