【LeetCode周赛】第368场周赛

100106. 元素和最小的山形三元组 I 简单

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

如果下标三元组 (i, j, k) 满足下述全部条件,则认为它是一个 山形三元组 :

  • i < j < k i < j < k i<j<k
  • n u m s [ i ] < n u m s [ j ] 且 n u m s [ k ] < n u m s [ j ] nums[i] < nums[j] 且 nums[k] < nums[j] nums[i]<nums[j]nums[k]<nums[j]

请你找出 nums 中 元素和最小 的山形三元组,并返回其 元素和 。如果不存在满足条件的三元组,返回 -1 。

示例 1:

输入:nums = [8,6,1,5,3]
输出:9
解释:三元组 (2, 3, 4) 是一个元素和等于 9 的山形三元组,因为:

  • 2 < 3 < 4 2 < 3 < 4 2<3<4
  • n u m s [ 2 ] < n u m s [ 3 ] 且 n u m s [ 4 ] < n u m s [ 3 ] nums[2] < nums[3] 且 nums[4] < nums[3] nums[2]<nums[3]nums[4]<nums[3]

这个三元组的元素和等于 nums[2] + nums[3] + nums[4] = 9 。可以证明不存在元素和小于 9 的山形三元组。

示例 2:

输入:nums = [5,4,8,7,10,2]
输出:13
解释:三元组 (1, 3, 5) 是一个元素和等于 13 的山形三元组,因为:

  • 1 < 3 < 5 1 < 3 < 5 1<3<5
  • n u m s [ 1 ] < n u m s [ 3 ] 且 n u m s [ 5 ] < n u m s [ 3 ] nums[1] < nums[3] 且 nums[5] < nums[3] nums[1]<nums[3]nums[5]<nums[3]

这个三元组的元素和等于 nums[1] + nums[3] + nums[5] = 13 。可以证明不存在元素和小于 13 的山形三元组。

示例 3:

输入:nums = [6,5,4,3,4,5]
输出:-1
解释:可以证明 nums 中不存在山形三元组。

提示:

  • 3 < = n u m s . l e n g t h < = 50 3 <= nums.length <= 50 3<=nums.length<=50
  • 1 < = n u m s [ i ] < = 50 1 <= nums[i] <= 50 1<=nums[i]<=50

分析:
数据量很小,可以进行三重循环的枚举。
同时与第二题内容一致,只是数据量小很多,具体方法见下一题

代码:
下一题


100114. 元素和最小的山形三元组 II 中等

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

如果下标三元组 (i, j, k) 满足下述全部条件,则认为它是一个 山形三元组 :

  • i < j < k i < j < k i<j<k
  • n u m s [ i ] < n u m s [ j ] nums[i] < nums[j] nums[i]<nums[j] n u m s [ k ] < n u m s [ j ] nums[k] < nums[j] nums[k]<nums[j]

请你找出 nums 中 元素和最小 的山形三元组,并返回其 元素和 。如果不存在满足条件的三元组,返回 -1 。

示例 1:

输入:nums = [8,6,1,5,3]
输出:9
解释:三元组 (2, 3, 4) 是一个元素和等于 9 的山形三元组,因为:

  • 2 < 3 < 4
  • nums[2] < nums[3] 且 nums[4] < nums[3]

这个三元组的元素和等于 nums[2] + nums[3] + nums[4] = 9 。可以证明不存在元素和小于 9 的山形三元组。

示例 2:

输入:nums = [5,4,8,7,10,2]
输出:13
解释:三元组 (1, 3, 5) 是一个元素和等于 13 的山形三元组,因为:

  • 1 < 3 < 5
  • nums[1] < nums[3] 且 nums[5] < nums[3]

这个三元组的元素和等于 nums[1] + nums[3] + nums[5] = 13 。可以证明不存在元素和小于 13 的山形三元组。

示例 3:

输入:nums = [6,5,4,3,4,5]
输出:-1
解释:可以证明 nums 中不存在山形三元组。

提示:

  • 3 < = n u m s . l e n g t h < = 1 0 5 3 <= nums.length <= 10^5 3<=nums.length<=105
  • 1 < = n u m s [ i ] < = 1 0 8 1 <= nums[i] <= 10^8 1<=nums[i]<=108

分析:
数据量达到了 1 0 5 10^5 105,不能使用三重循环进行枚举了。
注意山形三元组的条件:

  • i < j < k i < j < k i<j<k
  • n u m s [ i ] < n u m s [ j ] nums[i] < nums[j] nums[i]<nums[j] n u m s [ k ] < n u m s [ j ] nums[k] < nums[j] nums[k]<nums[j]

因此,先计算对应j+1~n的最小值,枚举 n u m s [ j ] nums[j] nums[j],同时不断维护0 ~ j-1 的最小nums[i](维护前后缀最小值)。比较最小的山形三元组的和。

代码:

class Solution {
public:
    int minimumSum(vector<int>& nums) {
        int mi=nums[0];
        int m = *max_element(nums.begin(),nums.end());
        int ans= m * 3;
        vector<int> min_nums(nums.size(),nums[nums.size()-1]);
        for(int i=nums.size()-2;i>=0;i--){
            min_nums[i]=min(min_nums[i+1], nums[i]);
        }
        for(int i=1;i<nums.size()-1;i++){
            if(mi<nums[i]&&min_nums[i+1]<nums[i]){
                cout<<mi<<", "<<nums[i]<<","<<min_nums[i+1]<<endl;
                ans=min(mi+nums[i]+min_nums[i+1], ans);
            }
            mi=min(mi,nums[i]);
        }
        if(ans==m*3) return -1;
        return ans;
    }
};


2910. 合法分组的最少组数 中等

给你一个长度为 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 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
  • 1 < = n u m s [ i ] < = 1 0 9 1 <= nums[i] <= 10^9 1<=nums[i]<=109

分析:
题目中的数组的具体值不重要,重要的是每一类数字出现的次数,因此需要先统计每一类数字在数组中出现的次数。
根据题意可得:最终分组中只有两种长度,假设较大值为a,较小值为b:a=b+1。组的长度是很重要的!根据贪心,为使分组数更少,因此要尽量使长度为a的组更多。

很容易想到nums中出现次数最少得数字的出现次数 n 即为,分组长度其一。另外一个值则为 n-1 或者 n+1

但存在一种情况:当数组中只出现两类数字,其对应出现次数为:{10, 12},此时无论是[9,10]还是[10,11],都不能将其正确划分,因此需要划分成长度更小的组。但更小的长度我们难以直接计算得到,只知道一个条件:[1, n],因此我们n开始对其进行枚举,直至找到对应的结果。

时间复杂度(题解中的分析):记录数字出现次数:O(n),。后续枚举:假设有k类数字,最小的次数为m,因此为O(km)。
总时间复杂度T(n) = O(n) + O(km) = O(n)

代码:
我写的代码,能通过,但存在一定的问题:

class Solution {
public:
    int ma(vector<int>& v,int mi){
        int i=0,ans=0;
        for(;i<v.size();i++){
            int t1=v[i]/mi;
            int t2=v[i]%mi;
            int t3=v[i]%(mi-1);
            if(t2!=0&&t2<mi-1){
                while(t1--){
                    t2+=mi;
                    if(t2%(mi-1)==0) break;
                }
                if(t2%(mi-1)==0) ans+=(t1+(t2/(mi-1)));
                else if(t3!=0) return -1;
                else{
                    ans+=v[i]/(mi-1);
                }
            }else ans+=(t1+(t2==0?0:1));
            
        }
        return ans;
    }

    int minGroupsForValidAssignment(vector<int>& nums) {
        unordered_map<int,int> m;
        priority_queue<int, vector<int>, greater<int>> q;
        vector<int> v;
        int ans=0,mi=0;
        for(int& num : nums){
            m[num]++;
            mi=max(mi,m[num]);
        }
        for(auto& [k, va] : m){
            v.push_back(va);
            mi=min(mi,va);
        }
        int tt=ma(v,mi+1);
        if(tt==-1) tt=ma(v,mi);
        else return tt;
        if(tt!=-1) return tt;
        for(int k=mi-1;k>1;k--){
            tt=ma(v,k);
            if(tt!=-1) return tt;
        }
        return tt;
    }
};

题解代码:

class Solution {
public:
    int minGroupsForValidAssignment(vector<int>& nums) {
        unordered_map<int, int> m;
        for(const int& num : nums){
            m[num]++;
        }
        // 求最小的value
        int k = min_element(m.begin(), m.end(), [](const auto& a, const auto& b){return a.second < b.second;})->second;
        for(;;k--){
            int ans=0;
            for(auto& [_,v] : m){
                if(v/k < v%k){
                    ans=0;break;
                }
                // 不会多出一个分组,原本余数为0~k,+k之后,余数变为k~2k。若原来为0,则此时结果不会+1,如果原本余数>1,则结果会多1(原本就要多分一组)
                ans+=(v+k)/(k+1);
            }
            if(ans) return ans;
        }
    }
};


2911. 得到 K 个半回文串的最少修改次数 困难

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

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

注意:
如果一个字符串从左往右和从右往左读是一样的,那么它是一个 回文串
如果长度为 len 的字符串存在一个满足 1 < = d < l e n 1 <= d < len 1<=d<len 的正整数 d , l e n % d = = 0 len \% d == 0 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 . l e n g t h < = 200 2 <= s.length <= 200 2<=s.length<=200
  • 1 < = k < = s . l e n g t h / 2 1 <= k <= s.length / 2 1<=k<=s.length/2
  • s s s 只包含小写英文字母。

分析:
半回文串的长度>1。如果子串的长度为1,此时不存在d,满足 1 < = d < l e n 1 <= d < len 1<=d<len
直接暴力枚举时间复杂度过高,肯定会超时,因此暴力枚举是行不通的。

困难题,不会。查看 题解 之后,虽然还是不太理解,但也有一定的收获。

记忆化搜索

  • d,一定是len的因子,因此可以对 1~s.length 所有数进行因数的计算,并保存,便于后续计算。
  • 对于 i~j 的子串,可以通过对修改字符的半回文串的判断,来储存需要修改的字符的数量,便于搜索。
  • dfs(i, j) 代表将 0~j 所有的字符分成 i+1 个半回文串的最小的修改次数,记录搜索过程中计算的最小修改次数,从而减少时间花销。
const int MX = 201;
vector<vector<int>> factors(MX);

int init = [] {
    for (int i = 1; i < MX; i++) {
        for (int j = i * 2; j < MX; j += i) {
            factors[j].push_back(i);
        }
    }
    return 0;
}();

class Solution {
public:

    int get_cut(string s){
        int n=s.length();
        int res = n;
        for(int& d : factors[n]){
            int cnt = 0;
            // i代表len%d的余数。通过余数可以找到对应下标对d求余相等的所有字符。
            for(int i=0;i<d;i++){
            	// 对于 adbgad ,长度6有因数1,2,3,6
            	// 1的话,其本身不为回文串
            	// 2: 判断s[0] s[2] s[4],s[1] s[3] s[5]是否都为回文串
            		// n%d==0,n就是多少倍的d。
            		// j表示余数为i的较小一半的字符(初始为i),即第一个索引余数为i的字符
            		// k表示余数为k的较大一半的字符(初始为n-d+i),即最后一个索引余数为i的字符
            		// 每次j+=d,k-=d,就可以对比下一对余数为i的字符
                for(int j=i,k=n-d+i;j<k;j+=d,k-=d){
                    if(s[j]!=s[k]) cnt++;
                }
            }
            res=min(res,cnt);
        }
        return res;
    }

    int dfs(int i, int j,  vector<vector<int>>& cut,  vector<vector<int>>& memo,int n){
    	// i为0,即将0~j这一子串分成1个半回文串的最小修改次数,直接返回cut[i][j]
        if(i==0) return cut[i][j];
        // memo[i][j]初始化为n+1,若其值<=n则说明已经计算了他的值,直接返回
        if(memo[i][j]<=n) return memo[i][j];
        // 暂未计算,下面对memo[i][j]进行计算
        // k最小为2*i: 因为最小的半回文串长度为2,需要将0~k-1的字符串分解成i个子串串,因此必须>=2*i
        // k最大为j-1:要保证当前分割出来的子串>=2
        for(int k=j-1;k>=2*i;k--){
            memo[i][j]=min(memo[i][j], dfs(i-1, k-1, cut, memo, n)+cut[k][j]);
        }
        return memo[i][j];
    }

    int minimumChanges(string s, int k) {
        int n=s.length();
        // cut[i][j], 代表将i~j这一子串,变为半回文串的修改次数
        vector<vector<int>> cut(n, vector<int>(n));
        // memo[i][j], 代表将 0~j 分成i+1个半回文串的最小修改次数
        // memo[i][j]初始化的值为n+1,因为子串最大为s本身,而且最多修改的次数也只能为n,因此n+1表示还没有计算memo[i][j]
        vector<vector<int>> memo(k, vector<int>(n, n+1));
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){
            	// 先计算i~j这一子串变成半回文串需要修改的次数
                cut[i][j]=get_cut(s.substr(i, j-i+1));
            }
        }
        // 进行记忆化搜索,即计算将 0~n-1,将其分为k个子串的最小修改次数
        return dfs(k-1,n-1, cut, memo, n);
    }
};

动态规划
根据递推公式得到的:

  • dp[i][j]:与记忆化搜索的dfs(i, j)性质一样,代表将 0~j 所有的字符分成 i+1 个半回文串的最小的修改次数。
  • cut[i][j]:对于 i~j 的子串,储存需要修改的字符的数字,来使s[i…j]变为半回文子串。
  • 递推公式: d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i − 1 ] [ l − 1 ] + c u t [ l ] [ j ] ) dp[i][j] = min(dp[i][j], dp[i-1][l-1] + cut[l][j]) dp[i][j]=min(dp[i][j],dp[i1][l1]+cut[l][j])
  • 当然 d p [ 0 ] [ j ] = c u t [ 0 ] [ j ] dp[0][j] = cut[0][j] dp[0][j]=cut[0][j]

这里可以利用从大到小的来遍历,将dp进行降维。

const int MX = 201;
vector<vector<int>> factors(MX);

int init = [] {
    for (int i = 1; i < MX; i++) {
        for (int j = i * 2; j < MX; j += i) {
            factors[j].push_back(i);
        }
    }
    return 0;
}();

class Solution {
public:
    int get_cut(string s){
        int n=s.length();
        int res = n;
        for(int& d : factors[n]){
            int cnt = 0;
            for(int i=0;i<d;i++){
                for(int j=i,k=n-d+i;j<k;j+=d,k-=d){
                    if(s[j]!=s[k]) cnt++;
                }
            }
            res=min(res,cnt);
        }
        return res;
    }

    int minimumChanges(string s, int k) {
        int n=s.length();
        vector<vector<int>> cut(n-1, vector<int>(n));
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){
                cut[i][j]=get_cut(s.substr(i, j-i+1));
            }
        }
        // 需要对其进行初始化,代表将0~j划分为1个半回文串的最小修改数为cut[0][j]
        vector<int> dp(cut[0]);
        
        // i:表示将0~j划分为i+1个半回文子串的最小修改数
        for(int i=1;i<k;i++){
        	// 前面i次的最小修改数已经计算完成,因此只需要计算第i+1次的修改数,加上之前的修改数
            // j:此次将划分的上限,即子串的最后一个字符。需要为后续的其他子串留下一定的满足条件的字符,
            for(int j = n-1-(k-i-1)*2;j>i*2;j--){
                dp[j]=INT_MAX;
                // l:代表从l~j划分为一个半回文串
                cout<<dp[j]<<endl;
                for(int l=i*2; l<j;l++){
                    dp[j]=min(dp[j], dp[l-1] + cut[l][j]);
                }
            }
        }
        return dp[n-1];
    }
};

最后一题的记忆化搜索和动态规划蛮难懂得,目前还只是简单的阅读了代码,还没有对其完全的理解。估计再遇到我也写不出,看题解其实也没有完全理解这道题的做法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值