难度简单0

给你一个整数数组 prices ,它表示一个商店里若干巧克力的价格。同时给你一个整数 money ,表示你一开始拥有的钱数。

你必须购买 恰好 两块巧克力,而且剩余的钱数必须是 非负数 。同时你想最小化购买两块巧克力的总花费。

请你返回在购买两块巧克力后,最多能剩下多少钱。如果购买任意两块巧克力都超过了你拥有的钱,请你返回 money 。注意剩余钱数必须是非负数。

示例 1:

输入:prices = [1,2,2], money = 3
输出:0
解释:分别购买价格为 1 和 2 的巧克力。你剩下 3 - 3 = 0 块钱。所以我们返回 0 。

示例 2:

输入:prices = [3,2,3], money = 3
输出:3
解释:购买任意 2 块巧克力都会超过你拥有的钱数,所以我们返回 3 。

提示:

  • 2 <= prices.length <= 50
  • 1 <= prices[i] <= 100
  • 1 <= money <= 100

贪心

class Solution {
    public int buyChoco(int[] prices, int money) {
        Arrays.sort(prices);
        if(prices[0] +prices[1] > money)
            return money;
        return money - prices[0] - prices[1];
    }
}

2707. 字符串中的额外字符

难度中等0

给你一个下标从 0 开始的字符串 s 和一个单词字典 dictionary 。你需要将 s 分割成若干个 互不重叠 的子字符串,每个子字符串都在 dictionary 中出现过。s 中可能会有一些 额外的字符 不在任何子字符串中。

请你采取最优策略分割 s ,使剩下的字符 最少 。

示例 1:

输入:s = "leetscode", dictionary = ["leet","code","leetcode"]
输出:1
解释:将 s 分成两个子字符串:下标从 0 到 3 的 "leet" 和下标从 5 到 8 的 "code" 。只有 1 个字符没有使用(下标为 4),所以我们返回 1 。

示例 2:

输入:s = "sayhelloworld", dictionary = ["hello","world"]
输出:3
解释:将 s 分成两个子字符串:下标从 3 到 7 的 "hello" 和下标从 8 到 12 的 "world" 。下标为 0 ,1 和 2 的字符没有使用,所以我们返回 3 。

提示:

  • 1 <= s.length <= 50
  • 1 <= dictionary.length <= 50
  • 1 <= dictionary[i].length <= 50
  • dictionary[i] 和 s 只包含小写英文字母。
  • dictionary 中的单词互不相同。

记忆化搜索

class Solution {
    Set<String> set;
    int res; 
    int[][] cache;
    String s;
    public int minExtraChar(String s, String[] dictionary) {
        set = new HashSet<>();
        for(String d : dictionary){
            set.add(d);
        }
        this.s = s;
        res = s.length();
        cache = new int[res + 1][res + 1];
        for(int i = 0; i < res; i++){
            Arrays.fill(cache[i], -1);
        }
        return dfs(0, s.length());
    }
    // 定义 dfs(i, last) 表示为枚举到s[i]时,s采取最优分割策略,剩下last个字符,那么
    // 若当前s[i]不参与分割 dfs(i, last) = dfs(i+1, last)
    // 若当前s[i]参与分割,以s[i]为分割首字母:dfs(i, last) = dfs(j+1, last - (j+1-i))
    //							,其中j满足(set.contains(s[i,j+1]) and j < len(s)) 
    public int dfs(int i, int last){
        if(i == s.length()){
            return last;    
        }
        if(cache[i][last] != -1) return cache[i][last];
        int res = dfs(i+1, last); // 当前字符不分割
        // 当前字符分割
        for(int j = i; j < s.length(); j++){
            if(set.contains(s.substring(i, j+1)))
                res = Math.min(res, dfs(j+1, last - (j+1 - i)));
        }
        return cache[i][last] = res;
    }
}

记忆化搜索 ==> DP(递推)

不会记忆化转递推😭

换一种思路思考记忆化搜索

对于同个状态可以从多个状态转移过来的情况,用dp写是最好的,dp[i]表示从i开始到n的子字符串最少需要移除多少个字符,转移方程为

dp[i] = dp[i + 1] + 1 直接移除第i个字符,从i+1转移dp[i] = min(dp[j]) if s[i:j] in d for j in range(i + 1,n) 不移除第i个字符,需要在d中找到子字符串匹配一起删除

class Solution {
    Set<String> set;
    int res; 
    int[] cache;
    String s;
    public int minExtraChar(String s, String[] dictionary) {
        set = new HashSet<>();
        for(String d : dictionary){
            set.add(d);
        }
        this.s = s;
        res = s.length();
        cache = new int[res + 1];
        Arrays.fill(cache, -1);
        return dfs(0);
    }
	// 记忆化搜索时间复杂度与状态个数有关
    public int dfs(int i){ // O(n) n个状态
        if(i == s.length())
            return 0; // 表示当前不需要移除字符了
        if(cache[i] >= 0) return cache[i];
        int ans = dfs(i+1) + 1; // 移除当前 i 位置字符,从dfs(i+1)转移
        // 不移除当前i位置字符,需要从d中转移
        for(int j = i; j < s.length(); j++){ // O(n)
            if(set.contains(s.substring(i, j+1))) // O(n)
                ans = Math.min(ans, dfs(j+1));
        }
        return cache[i] = ans;
    }
}

时间复杂度:O(n^3)

转为递推

class Solution {
    // 定义 f(i) 表示只分割前 i 个字符,最后能剩下几个字符
    // 状态转移方程:
    //  f(i) = f(i-1) + 1, 这个字符时剩下的
    //  f(i) = f(j),若s[j+1, i] 在字典中
    // 两者取最小值,即:f(i) = min(f(i-1)+1, f(j))
    // 初值 f(0) = 0
    // 最后返回f(n)
    public int minExtraChar(String s, String[] dictionary) {
        Set set = new HashSet<>();
        for(String d : dictionary){
            set.add(d);
        }
        int n = s.length();
        int[] f = new int[n+1];
        for(int i = 1; i <= n; i++){
            f[i] = Integer.MAX_VALUE;
        }
        f[0] = 0;
        for(int i = 1; i <= n; i++){
            // i不参与分割,直接转移
            f[i] = f[i-1] + 1;
            // i参与分割,枚举所有[0,i]为起点的j
            for(int j = 0; j < i; j++){
                if(set.contains(s.substring(j, i)))
                    f[i] = Math.min(f[i], f[j]);
            }
        }
        return f[n];
    }
}

2708. 一个小组的最大实力值

难度中等0

给你一个下标从 0 开始的整数数组 nums ,它表示一个班级中所有学生在一次考试中的成绩。老师想选出一部分同学组成一个 非空 小组,且这个小组的 实力值 最大,如果这个小组里的学生下标为 i0, i1, i2, … , ik ,那么这个小组的实力值定义为 nums[i0] * nums[i1] * nums[i2] * ... * nums[ik] 。

请你返回老师创建的小组能得到的最大实力值为多少。

示例 1:

输入:nums = [3,-1,-5,2,5,-9]
输出:1350
解释:一种构成最大实力值小组的方案是选择下标为 [0,2,3,4,5] 的学生。实力值为 3 * (-5) * 2 * 5 * (-9) = 1350 ,这是可以得到的最大实力值。

示例 2:

输入:nums = [-4,-5,-4]
输出:20
解释:选择下标为 [0, 1] 的学生。得到的实力值为 20 。我们没法得到更大的实力值。

提示:

  • 1 <= nums.length <= 13
  • -9 <= nums[i] <= 9

排序 + 贪心

排序 + 贪心 小组最大实力值与元素位置无关,先排序,排序后,从左到右都是从负到正,从左边开始遍历到0值,若有两个负数则相乘计算答案,越小的负数相乘对答案贡献越大。从右边开始遍历乘上所有正数。

class Solution {
    public long maxStrength(int[] nums) {
        if(nums.length == 1) return nums[0];
        Arrays.sort(nums);
        long ans = 1l;
        for(int i = 0; i+1 < nums.length && nums[i] < 0 && nums[i+1] < 0; i += 2){
            ans *= nums[i] * nums[i+1];
        }
        // // 特判:[0,-1]、[-4,-5,-4]、[-1, -1]
        if((ans == 1l && nums[nums.length-1] <= 0) && (nums[1] >= 0)) return nums[nums.length-1];
        for(int i = nums.length-1; i >= 0 && nums[i] > 0; i -= 1){
            ans *= nums[i];
        }
        return ans;
    }
}

DFS枚举所有子集

class Solution {
    // 枚举所有子集,计算最大答案
    long ans = Long.MIN_VALUE;
    int[] nums;
    public long maxStrength(int[] nums) {
        this.nums = nums;
        dfs(0, 1l, true);
        return ans;
    }

    // 因为要去掉空集,使用is_empty判断是否为空集,初始状态下是空集
    public void dfs(int i, long prod, boolean is_empty){
        if(i == nums.length){
            if(!is_empty)
                ans = Math.max(ans, prod);
            return;
        }
        // 不选
        dfs(i+1, prod, is_empty);
        // 选
        dfs(i+1, prod * nums[i], false);
    }
}

动态规划(O(n))

class Solution {
    /**
    选或不选模型 ==> 动态规划:
        由于有负数,考虑最小值和最大值:
        最大值有四种情况转移:
            mx[i] = mx[i-1]             不选nums[i]
                  = nums[i]             nums[i] 自成最大值
                  = mx[i-1] * nums[i]   选nums[i]
                  = mn[i-1] * nums[i]   选nums[i]
     */
    public long maxStrength(int[] nums) {
        long max = nums[0], min = nums[0];
        for(int i = 1; i < nums.length; i++){
            long tmp = max;
            max = Math.max(Math.max(max, nums[i]), Math.max(max * nums[i], min * nums[i]));
            min = Math.min(Math.min(min, nums[i]), Math.min(tmp * nums[i], min * nums[i]));
        }
        return max;
    }
}

2709. 最大公约数遍历

难度困难1

给你一个下标从 0 开始的整数数组 nums ,你可以在一些下标之间遍历。对于两个下标 i 和 j(i != j),当且仅当 gcd(nums[i], nums[j]) > 1 时,我们可以在两个下标之间通行,其中 gcd 是两个数的 最大公约数 。

你需要判断 nums 数组中 任意 两个满足 i < j 的下标 i 和 j ,是否存在若干次通行可以从 i 遍历到 j 。

如果任意满足条件的下标对都可以遍历,那么返回 true ,否则返回 false 。

示例 1:

输入:nums = [2,3,6]
输出:true
解释:这个例子中,总共有 3 个下标对:(0, 1) ,(0, 2) 和 (1, 2) 。
从下标 0 到下标 1 ,我们可以遍历 0 -> 2 -> 1 ,我们可以从下标 0 到 2 是因为 gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1 ,从下标 2 到 1 是因为 gcd(nums[2], nums[1]) = gcd(6, 3) = 3 > 1 。
从下标 0 到下标 2 ,我们可以直接遍历,因为 gcd(nums[0], nums[2]) = gcd(2, 6) = 2 > 1 。同理,我们也可以从下标 1 到 2 因为 gcd(nums[1], nums[2]) = gcd(3, 6) = 3 > 1 。

示例 2:

输入:nums = [3,9,5]
输出:false
解释:我们没法从下标 0 到 2 ,所以返回 false 。

示例 3:

输入:nums = [4,3,12,8]
输出:true
解释:总共有 6 个下标对:(0, 1) ,(0, 2) ,(0, 3) ,(1, 2) ,(1, 3) 和 (2, 3) 。所有下标对之间都存在可行的遍历,所以返回 true 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

分解质因数 + 并查集

把 nums 中的每个位置看成一个点,把所有质数也都看成一个点。如果 nums[i] 被质数 p 整除,那么从位置点 i 向质数点 p 连一条边。因为每个数至多只能被 log⁡ 个质数整除,因此连边的总数是 O(nlogA) 的。

这样,问题就变为:检查所有位置点是否处于同一连通块内。用并查集解决即可。


nums 数组中 任意 两个满足 i < j 的下标 i 和 j ,是否存在若干次通行可以从 i 遍历到 j 。其实换句话说就是判断这个nums数组是不是整体是一个连通分量,这个适合用并查集来做。

我们考虑什么情况下可以进行连边,很明显两两之间去算gcd然后去连边这部分的时间复杂度是O(n^2)的,无法通过本题。

我们可以让每个nums中的数分别与自身的质因数之间连一条边,这样当gcd(nums[i],nums[j]) >= 2时,这时候i和j一定会通过一个质因数间接将他们连接起来。

代码实现的时候用了埃氏筛去做区间范围内每个数的快速求质因数,并查集中前MX个节点为质因数,后n个节点为数组中的下标,通过质因数的方式间接连接,最后判断连通性即可。

https://leetcode.cn/circle/discuss/lOcuHV/

class Solution {
    private class DSU{
        int[] parent;

        public DSU(int N) {
            parent = new int[N];
            for (int i = 0; i < N; ++i) {
                parent[i] = i;
            }
        }

        public int find(int x) {
            if (parent[x] != x) parent[x] = find(parent[x]);
            return parent[x];
        }

        public void union(int x, int y) {
            parent[find(x)] = find(y);
        }
    }

    public boolean canTraverseAllPairs(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        DSU uf = new DSU(nums.length);
        for (int i = 0; i < nums.length; ++i) {
            int t = nums[i];
            for (int j = 2; j * j <= t; ++j) {
                while (t % j == 0) {
                    t /= j;
                    if (!map.containsKey(j)) {
                        map.put(j, i);
                    } else {
                        uf.union(map.get(j), i);
                    }
                }
            }
            if (t != 1) {
                if (!map.containsKey(t)) {
                    map.put(t, i);
                } else {
                    uf.union(map.get(t), i);
                }
            }
        }

        int f = uf.find(0);
        for (int i = 0; i < nums.length; ++i) {
            if (uf.find(i) != f) {
                return false;
            }
        }
        return true;
    }
}