Leetcode 第368场周赛 三四题

第三题:

给你一个长度为 n 下标从 0 开始的整数数组 nums 。

我们想将下标进行分组,使得 [0, n - 1] 内所有下标 i 都 恰好 被分到其中一组。

如果以下条件成立,我们说这个分组方案是合法的:

  • 对于每个组 g ,同一组内所有下标在 nums 中对应的数值都相等。
  • 对于任意两个组 g1 和 g2 ,两个组中 下标数量 的 差值不超过 1 。

请你返回一个整数,表示得到一个合法分组方案的 最少 组数。

示例 1:

输入:nums = [3,2,3,2,3]
输出:2
解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标:
组 1 -> [0,2,4]
组 2 -> [1,3]
所有下标都只属于一个组。
组 1 中,nums[0] == nums[2] == nums[4] ,所有下标对应的数值都相等。
组 2 中,nums[1] == nums[3] ,所有下标对应的数值都相等。
组 1 中下标数目为 3 ,组 2 中下标数目为 2 。
两者之差不超过 1 。
无法得到一个小于 2 组的答案,因为如果只有 1 组,组内所有下标对应的数值都要相等。
所以答案为 2 。

示例 2:

输入:nums = [10,10,10,3,1,1]
输出:4
解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标:
组 1 -> [0]
组 2 -> [1,2]
组 3 -> [3]
组 4 -> [4,5]
分组方案满足题目要求的两个条件。
无法得到一个小于 4 组的答案。
所以答案为 4 。

提示:

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

这道题的关键在于将nums数组转换成次数数组之后,怎么解决不同次数的分割问题。

首先利用HashMap遍历nums数组,我们可以得到一个各个数字以及出现次数的键值对cnt.题目中要求每个划分的数量差值最大为1,所以关键是最小值的那个键值对。我们记录最小value键值对为min,通过遍历cnt.value()我们可以得到这个最小值。

但是我们得到这个最小值怎么用呢?如果用min,min-1或者min+1去划分剩下的value,那么就有可能会出现这样一种情况:cnt = {3,4,5} 如果用3去划分,就出现{3,3,1,3,2},显然3-1 = 2,不成立。同理,4也不行。min-1在本案例中可以。但是在更加复杂的用例中可能出现需要min-2甚至min-3的情况,这就没法解决了。那么有什么好的解法呢?且看大佬解法:

lass Solution {
    public int minGroupsForValidAssignment(int[] nums) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : nums) {
            cnt.merge(x, 1, Integer::sum);
        }
        int k = nums.length;
        for (var c : cnt.values()) {
            k = Math.min(k, c);
        }
        for (; ; k--) {
            int ans = 0;
            for (int c : cnt.values()) {
                if (c / k < c % k) {
                    ans = 0;
                    break;
                }
                ans += (c + k) / (k + 1);
            }
            if (ans > 0) {
                return ans;
            }
        }
    }
}

是否有点看不懂呢? 这里我来详细解说一下。首先一样的求出这个数量散列,再遍历value值得到最小的值k。

有一个很显然的定理,当一个key的出现次数c%k <= floor(c/k)时,我们可以将c%k多出来的数量平均分配到总组数floor(c/k)当中去。如:32 % 10 = 2, 32/10=3,那么我们就可以把2平分到两个组中,这样得到的结果就是11,11,10。最大值11也就比k大。但是如果c%k>floor(c/k)时我们就不能这样干了。只能寻求k<10以后的划分了。如{3,4,5}需要寻求2的划分一样。

为了防止k-1甚至k-2都无法完成划分,我们安排k--循环来求解。每次遍历都意味着新的划分,因此我们每次循环维持一个记录数ans=0。这时我们就可以遍历所有value值了。当c/k < c%k时,我们就需要跳出循环了。同时因为无法划分完成,我们需要将ans值置零。需要等待k--。若c/k >= c%k,那么我们可以进行划分,但是能够划分成多少组呢?答案是ceil(c/k)。以下是简单表示ceil(c/k)的方式:

                                                        ceil(c / k) = (c + k) / (k + 1)

当我们循环完成没有跳出,就可以返回划分值了。题解完毕。

第四题:

给你一个字符串 s 和一个整数 k ,请你将 s 分成 k 个 子字符串 ,使得每个 子字符串 变成 半回文串 需要修改的字符数目最少。

请你返回一个整数,表示需要修改的 最少 字符数目。

注意:

  • 如果一个字符串从左往右和从右往左读是一样的,那么它是一个 回文串 。
  • 如果长度为 len 的字符串存在一个满足 1 <= d < len 的正整数 d ,len % d == 0 成立且所有对 d 做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串 ,那么我们说这个字符串是 半回文串 。比方说 "aa" ,"aba" ,"adbgad" 和 "abab" 都是 半回文串 ,而 "a" ,"ab" 和 "abca" 不是。
  • 子字符串 指的是一个字符串中一段连续的字符序列。

示例 1:

输入:s = "abcac", k = 2
输出:1
解释:我们可以将 s 分成子字符串 "ab" 和 "cac" 。子字符串 "cac" 已经是半回文串。如果我们将 "ab" 变成 "aa" ,它也会变成一个 d = 1 的半回文串。
该方案是将 s 分成 2 个子字符串的前提下,得到 2 个半回文子字符串需要的最少修改次数。所以答案为 1 。

示例 2:

输入:s = "abcdef", k = 2
输出:2
解释:我们可以将 s 分成子字符串 "abc" 和 "def" 。子字符串 "abc" 和 "def" 都需要修改一个字符得到半回文串,所以我们总共需要 2 次字符修改使所有子字符串变成半回文串。
该方案是将 s 分成 2 个子字符串的前提下,得到 2 个半回文子字符串需要的最少修改次数。所以答案为 2 。

示例 3:

输入:s = "aabbaa", k = 3
输出:0
解释:我们可以将 s 分成子字符串 "aa" ,"bb" 和 "aa" 。
字符串 "aa" 和 "bb" 都已经是半回文串了。所以答案为 0 。

提示:

  • 2 <= s.length <= 200
  • 1 <= k <= s.length / 2
  • s 只包含小写英文字母。
class Solution {
    final int mx = 201; 
    List<Integer>[] divisors = new List[201];
    public int minimumChanges(String s, int k) {
        Arrays.setAll(divisors,t->new LinkedList<>());
        for(int i=1;i<mx;i++){
            for(int j=i*2;j<mx;j+=i){
                divisors[j].add(i);
            }
        }

        int n = s.length();
        int[][] modify = new int[n-1][n];
        for(int l = 0;l <n-1;l++){
            for(int r = l+1; r<n; r++){
                modify[l][r] = getModify(s.substring(l,r+1));
            }
        }

        int[] f = modify[0];
        for(int i=1;i<k;i++){
            for(int j=n-1;j>i*2;j--){
                f[j] = Integer.MAX_VALUE;
                for(int L = i*2;L<j;L++){
                    f[j] = Math.min(f[j],f[L-1]+modify[L][j]);
                }
            }
        }
        return f[n-1];
    }
    private int getModify(String s){
        char[] chs = s.toCharArray();
        int n = chs.length;
        int res = n;
        for(int d : divisors[n]){
            int cnt = 0;
            for(int i0 = 0;i0 < d;i0++){
                for(int i=i0,j=n-d+i0;i<j;i+=d,j-=d){
                    if(chs[i] != chs[j]){
                        cnt++;
                    }
                }
            }
            res = Math.min(res,cnt);
        }
        return res;
    }
}

这题直接给我干破防了。。。看题解都看了好一会,真的是史诗级巨坑。但是其中的部分思路很值得我们学习。

如果长度为 len 的字符串存在一个满足 1 <= d < len 的正整数 d ,len % d == 0 成立且所有对 d 做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串 ,那么我们说这个字符串是 半回文串 。

根据半回纹串定义,我们可以知道,划分一个字符串时需要根据角标划分,而且是要用其除了1以外的公因数去划分,才能做到以上定义。根据题目要求中2 <= s.length <= 200,我们只用对200以内整数求公因数即可。这里我们用200维List存储公因数。

打表求公因数的时间复杂度太高,这里我们采用更加巧妙的方法:把除了1以外所有数从小到大先遍历一边,把所有公因数含有当前值的数的那一维List当中添加上这个数。当然,一个一个去找这个数是否含有该公因数还是暴力求解,所以我们直接从i*2开始(公因数不能是本身,因此含有该公因数的下一个元素就是2*i),然后向后以步长i进行遍历,这样遍历到的每一步都有该公因数。

Arrays.setAll(divisors,t->new LinkedList<>());
        for(int i=1;i<mx;i++){
            for(int j=i*2;j<mx;j+=i){
                divisors[j].add(i);
            }
        }

然后我们定义一个getModify()函数,用来计算传入字符串转变为半回纹串最少需要改动几步。我们需要先得到传入字符串长度n,然后在divisor[ n ]处找到所有的公因数。然后就每个公因数找到修改次数,最终返回最少修改次数返回。假设本次遍历到的公因数为d,为了找到修改处,我们需要判断前后元素是否相同。所以我们让i0从0遍历到d,用来记录遍历步长内元素。同时,我们用双指针i,j分别代表前和后的指针位置,步长为d。i从i0的位置出发,j从n-d+i0出发,这样就能保证d长内每一个元素都能被遍历到,同时每次遍历正好是前与后。当chs[.i ] != chs[ j ]时,就是做出修改的时候,cnt++。最后在第一层循环内比较不同公因数得到的修改次数,返回最小值。

private int getModify(String s){
        char[] chs = s.toCharArray();
        int n = chs.length;
        int res = n;
        for(int d : divisors[n]){
            int cnt = 0;
            for(int i0 = 0;i0 < d;i0++){
                for(int i=i0,j=n-d+i0;i<j;i+=d,j-=d){
                    if(chs[i] != chs[j]){
                        cnt++;
                    }
                }
            }
            res = Math.min(res,cnt);
        }
        return res;
    }

然后就是用备忘录方法,创建dp数组。200维,201列。dp数组modify[ i ][ j ]代表了从i到j处字符串的修改次数。

接下来就是重头戏了:

首先根据题目, 如果长度为 len 的字符串存在一个满足 1 <= d < len 的正整数 d ,len % d == 0 成立且所有对 d 做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串,那么我们说这个字符串是 半回文串 。我们可以得知,半回纹串至少长度为2。因为分成了k个半回纹串,因此切了k-1刀。假设一刀下去分割了长度为m部分,那么就有原来是(n , k)切一刀变成(n-m , k-1)。因为总共切n-1刀,就遍历n-1次。从1开始遍历,后面要用。为了保证每个子字符串有大于等于2 的长度,我们在切每一刀时,从后向前遍历,最小为i*2。同时再用一个数L从前向后遍历,起始位置也是i*2。同时根据状态转移方程f[j] = Math.min(f[j],f[L-1]+modify[L][j]);最终返回f[n-1]即可

int[] f = modify[0];
        for(int i=1;i<k;i++){
            for(int j=n-1;j>i*2;j--){
                f[j] = Integer.MAX_VALUE;
                for(int L = i*2;L<j;L++){
                    f[j] = Math.min(f[j],f[L-1]+modify[L][j]);
                }
            }
        }
        return f[n-1];

参考:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值