LeetCode --- 443周赛

题目列表

Q1. 到达每个位置的最小费用
Q2. 子字符串连接后的最长回文串 I
Q3. 子字符串连接后的最长回文串 II
Q4. 使 K 个子数组内元素相等的最少操作数

一、到达每个位置的最小费用

题目要求返回从队尾到达任意位置的最小费用,规则:如果下标 i i i 在前面,则与 i i i 交换位置需要 c o s t [ i ] cost[i] cost[i],如果如果下标 i i i 在后面,交换位置不用支付费用。

贪心:对于任意位置 i i i,我们不能先去 j ( j > i ) j\ (j>i) j (j>i),再到 i i i,这样的方案不如直接到达 i i i,但是如果 j ≤ i j\le i j≤i,则可以考虑,因为从 j → i j \rightarrow i j→i 不用支付费用,所以这题的本质就是求前缀最小值

代码如下

cpp 复制代码
// C++
class Solution {
public:
    vector<int> minCosts(vector<int>& cost) {
        int n = cost.size();
        for(int i = 1; i < n; i++){
            cost[i] = min(cost[i], cost[i-1]);
        }
        return cost;
    }
};
python 复制代码
# Python
class Solution:
    def minCosts(self, cost: List[int]) -> List[int]:
        return list(accumulate(cost, min))

二、子字符串连接后的最长回文串 I & II

题目要求我们从字符串 s s s 和 t t t 中各自选出一个子串拼接成一个回文串

思路如下:

  • 分类讨论:设 s ′ s' s′ 是 s s s 的子串, t ′ t' t′ 是 t t t 的子串

    • 如果 l e n ( s ′ ) = l e n ( t ′ ) len(s')=len(t') len(s′)=len(t′),则将 t t t 反转之后,就是求 s s s 和 t t t 中最长公共子串,做法类似 718. 最长重复子数组,可以用动态规划来解决

      • 设 f [ i ] [ j ] f[i][j] f[i][j] 表示 s s s 中以 s [ i ] s[i] s[i] 为结尾的子串与 t t t 中以 t [ j ] t[j] t[j] 为起始的子串(倒序)的最长匹配长度
      • 如果 s [ i ] = = t [ j ] s[i]==t[j] s[i]==t[j],则 f [ i ] [ j ] = f [ i − 1 ] [ j + 1 ] + 2 f[i][j]=f[i-1][j+1]+2 f[i][j]=f[i−1][j+1]+2,否则 f [ i ] [ j ] = 0 f[i][j]=0 f[i][j]=0
      • 为了防止越界,将 i + 1 i+1 i+1,得 f [ i + 1 ] [ j ] = f [ i ] [ j + 1 ] + 2 f[i+1][j]=f[i][j+1]+2 f[i+1][j]=f[i][j+1]+2
    • 如果 l e n ( s ′ ) > l e n ( t ′ ) len(s')>len(t') len(s′)>len(t′),令 s ′ = s 1 ′ + s 2 ′ s'=s_1'+s_2' s′=s1′+s2′ 且 l e n ( s 1 ′ ) = l e n ( t ′ ) len(s_1')=len(t') len(s1′)=len(t′),则 s 1 ′ + s 2 ′ + t ′ s_1'+s_2'+\ t' s1′+s2′+ t′ 是回文的,所以 s 2 ′ s_2' s2′ 本身应该是回文的,而 s 1 ′ s_1' s1′ 和 t ′ t' t′ 依旧是求最长公共子串。这里我们用中心扩展法枚举 s 2 ′ s_2' s2′ 回文串,然后统计最长的 s 1 ′ + s 2 ′ + t ′ s_1'+s_2'+\ t' s1′+s2′+ t′。

      • 贪心:对于以 i i i (或者 i 、 i + 1 i、i+1 i、i+1) 为中心的回文串 s 2 ′ s_2' s2′,我们只要看最长的那个回文串就行。比如 . . . a b c b a . . . ...abcba... ...abcba...,以 c c c 为中心扩展,我们不需要统计 s 1 ′ + c + t ′ s_1'+c+t' s1′+c+t′ 和 s 1 ′ + b c b + t ′ s_1'+bcb+t' s1′+bcb+t′ 的长度,而只需要统计 s 1 ′ + a b c b a + t ′ s_1'+abcba+t' s1′+abcba+t′ 的长度即可
      • 因为对于相同长度的 s 1 ′ + s 2 ′ + t ′ s_1'+s_2'+\ t' s1′+s2′+ t′, s 2 ′ s_2' s2′ 越长,则 s 1 ′ s_1' s1′ 和 t ′ t' t′ 的长度就越短,则更有可能从字符串 s s s 和 t t t 中找出符合条件的 s 1 ′ s_1' s1′ 和 t ′ t' t′
      • 那么有没有可能 s 2 ′ s_2' s2′ 短, s 1 ′ s_1' s1′ 很长,导致 s 1 ′ + s 2 ′ + t ′ s_1'+s_2'+\ t' s1′+s2′+ t′ 的长度很长呢?不可能,以 . . . d d a b c b a e . . . ...ddabcbae... ...ddabcbae... 为例,设 s 1 ′ = . . . d d a b , s 2 ′ = c , t = b a d d . . . s_1'=...ddab,s_2'=c,t=badd... s1′=...ddab,s2′=c,t=badd... 和 s 1 ′ ′ = . . . d d , s 2 ′ ′ = a b c b a , t ′ ′ = d d . . . s_1''=...dd,s_2''=abcba,t''=dd... s1′′=...dd,s2′′=abcba,t′′=dd...,由于 s 1 ′ ′ s_1'' s1′′ 是 s 1 ′ s_1' s1′ 的前缀,所以 s 2 ′ s_2' s2′ 短, s 1 ′ s_1' s1′ 很长的策略最多和我们的贪心策略得到的长度一致,不可能优于我们的贪心策略,故我们的贪心是正确的
    • 如果 l e n ( s ′ ) < l e n ( t ′ ) len(s')<len(t') len(s′)<len(t′),同上

代码如下

cpp 复制代码
// C++
class Solution {
public:
    int longestPalindrome(string s, string t) {
        auto f = [](const string& s, const string& t)->int{
            int n = s.size(), m = t.size();
            int ans = 0;
            vector dp(n + 1, vector<int>(m + 1));
            vector<int> mx(n + 1); // mx[i+1] 记录以 s[i] 为结尾的子串与 t 中任意位置为起始位置的最长公共子串的擦汗高难度
            // dp[i][j] = dp[i-1][j+1] + 2 if s[i] == t[j]
            // 防止越界 dp[i+1][j] = dp[i][j+1] + 2 if s[i] == t[j]
            for(int i = 0; i < n; i++){
                for(int j = m - 1; j >= 0; j--){
                    if(s[i] == t[j]) dp[i+1][j] = dp[i][j+1] + 2;
                    mx[i+1] = max(dp[i+1][j], mx[i+1]);
                    ans = max(ans, dp[i+1][j]);
                }
            }
            for(int i = 0, l, r; i < n; i++){
            	// 以 i 为中心,进行中心扩展
                for(l = i - 1, r = i + 1; l >= 0 && r < n && s[l] == s[r]; l--, r++);
                ans = max(ans, r - l - 1 + mx[l+1]);
                // 以 i,i+1 为中心,进行中心扩展
                for(l = i, r = i + 1; l >= 0 && r < n && s[l] == s[r]; l--, r++);
                ans = max(ans, r - l - 1 + mx[l+1]);
            }
            return ans;
        };
        int ans = f(s, t);
        // 由于 len(s') < len(t') 和 len(s') < len(t') 是同一个逻辑,可以倒序后,复用代码逻辑
        ranges::reverse(s);
        ranges::reverse(t);
        return max(ans, f(t, s)); 
    }
};
python 复制代码
# Python
def max2(x:int, y:int)->int:
    return x if x > y else y
class Solution:
    def f(self, s:str, t:str)->int:
        n, m = len(s), len(t)
        dp = [[0]*(m+1) for _ in range(n+1)]
        mx = [0]*(n+1)
        ans = 0
        for i in range(n):
            for j in range(m-1, -1, -1):
                if s[i] == t[j]:
                    dp[i+1][j] = dp[i][j+1] + 2
                mx[i+1] = max2(mx[i+1], dp[i+1][j])
                ans = max2(ans, dp[i+1][j])
        for i in range(n):
            l = r = i
            while l >= 0 and r < n and s[l] == s[r]:
                l -= 1
                r += 1
            ans = max2(ans, r - l - 1 + mx[l+1])
            l, r = i, i + 1
            while l >= 0 and r < n and s[l] == s[r]:
                l -= 1
                r += 1
            ans = max2(ans, r - l - 1 + mx[l+1])
        return ans
    def longestPalindrome(self, s: str, t: str) -> int:
        return max2(self.f(s, t), self.f(t[::-1], s[::-1]))

三、使 K 个子数组内元素相等的最少操作数

看到将数组划分为多个子数组,很容易想到 划分型 D P 划分型DP 划分型DP,基本的状态定义为 f [ i ] [ j ] f[i][j] f[i][j] 表示将前 i i i 个数划分为 j j j 个子数组所需的最少操作次数。本题也是同理。

  • 动态规划

    • 状态定义:设 f [ i ] [ j ] f[i][j] f[i][j] 表示将前 i i i 个数划分出 j j j 个长度恰好为 x x x 的子数组(子数组中元素相同)所需的最少操作次数
    • 状态转移:依旧是选或不选的思路
      • 选以 n u m s [ i ] nums[i] nums[i] 为右端点的长度为 x x x 的子数组,则 f [ i ] [ j ] = f [ i − x ] [ j − 1 ] + o p s [ i ] f[i][j]=f[i-x][j-1]+ops[i] f[i][j]=f[i−x][j−1]+ops[i],其中 o p s [ i ] ops[i] ops[i] 表示让区间 [ i − x + 1 , i ] [i-x+1,i] [i−x+1,i] 内元素相同的最小操作次数
      • 不选以 n u m s [ i ] nums[i] nums[i] 为右端点的长度为 x x x 的子数组,则 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i−1][j]
      • 故 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − x ] [ j − 1 ] + o p s [ i ] ) f[i][j]=max(f[i-1][j],f[i-x][j-1]+ops[i]) f[i][j]=max(f[i−1][j],f[i−x][j−1]+ops[i])
    • 初始化
      • 当 j = 0 j=0 j=0 时, f [ i ] [ 0 ] = 0 f[i][0]=0 f[i][0]=0,因为不需要进行任何操作
      • 当 i < j ∗ x i<j*x i<j∗x 时, f [ i ] [ j ] = i n f f[i][j]=inf f[i][j]=inf,因为元素不足以分成 j j j 个长度为 x x x 的子数组
  • 如何求解 o p s [ i ] ? ops[i]\ ? ops[i] ?,即如何快速计算出将 [ i − x + 1 , i ] [i-x+1,i] [i−x+1,i] 区间中的数变成同一个数字的最小操作次数

    • 给定一些数字,每次可以对一个数进行 + 1 / − 1 +1/-1 +1/−1 的操作,让它们都变成哪个数字,进行的操作次数最小?中位数贪心

    • 如何求解操作次数?

      • 根据上图,操作次数为 x − n u m s [ 0 ] + x − n u m s [ 1 ] + . . . + x − n u m s [ j ] + n u m s [ j + 1 ] − x + n u m s [ j + 2 ] − x + . . . + n u m s [ n − 1 ] − x = ( j + 1 ) ∗ x − p r e [ j ] + p r e [ n ] − p r e [ j + 1 ] − ( n − j − 1 ) ∗ x , j = n / 2 x-nums[0]+x-nums[1]+...+x-nums[j]+nums[j+1]-x+nums[j+2]-x+...+nums[n-1]-x=(j+1)*x-pre[j]+pre[n]-pre[j+1]-(n-j-1)*x,j=n/2 x−nums[0]+x−nums[1]+...+x−nums[j]+nums[j+1]−x+nums[j+2]−x+...+nums[n−1]−x=(j+1)∗x−pre[j]+pre[n]−pre[j+1]−(n−j−1)∗x,j=n/2
      • 但是题目所给的数组是无序的,并且我们需要维护动态区间 [ i − x + 1 , i ] [i-x+1,i] [i−x+1,i] 中的中位数
        可以用对顶堆来维护第 n / 2 n/2 n/2 个数,即中位数,同时维护 p r e [ j ] 、 p r e [ n ] − p r e [ j + 1 ] pre[j]、pre[n]-pre[j+1] pre[j]、pre[n]−pre[j+1],即维护两个堆的元素和

具体代码如下

cpp 复制代码
// C++
class Solution {
    using LL = long long;
public:
    long long minOperations(vector<int>& nums, int x, int k) {
        // 对顶堆
        int K = (x + 1) / 2;
        multiset<int> stL, stR; // 由于 nums 中可能有重复元素,所以用 multiset
        LL sumL = 0, sumR = 0;
        auto add = [&](int x){
            // 优先向 stL 中插入数据
            if(stL.size() < K){
                stL.insert(x);
                sumL += x;
                return;
            }
            stR.insert(x);
            sumR += x;
            auto mn = stR.begin();
            auto mx = prev(stL.end());
            if(*mx > *mn){
                sumL += *mn - *mx;
                sumR += *mx - *mn;
                stL.insert(*mn);
                stR.insert(*mx);
                stL.erase(mx);
                stR.erase(mn);
            }
        };
        auto remove = [&](int x){
            auto it = stR.find(x);
            if(it != stR.end()){
                stR.erase(it);
                sumR -= x;
                return;
            }
            it = stL.find(x);
            stL.erase(it);
            sumL -= x;
            int mn = *stR.begin();
            stL.insert(mn);
            sumL += mn;
            stR.erase(stR.begin());
            sumR -= mn;
        };

        int n = nums.size();
        vector<LL> ops(n);
        for(int i = 0; i < n; i++){
            add(nums[i]);
            if(i >= x - 1){
                int mid = *stL.rbegin();
                ops[i] = 1LL * K * mid - sumL + sumR - 1LL * (x - K) * mid;
                remove(nums[i - x + 1]);
            }
        }
        vector f(n + 1, vector<LL>(k + 1, LLONG_MAX/2));
        for(int i = 0; i <= n; i++) f[i][0] = 0;
        for(int j = 1; j <= k; j++){
            for(int i = x*j; i <= n; i++){
                f[i][j] = min(f[i-1][j], f[i-x][j-1] + ops[i-1]);
            }
        }
        return f[n][k];
    }
};
相关推荐
山海不说话7 分钟前
从零搭建微服务项目Pro(第7-1章——分布式雪花算法)
分布式·算法·spring·微服务·架构
手握风云-21 分钟前
优选算法的妙思之流:分治——归并专题
数据结构·算法·排序算法
小样vvv27 分钟前
【面试篇】Es
elasticsearch·面试·职场和发展
梭七y35 分钟前
【力扣hot100题】(063)搜索二维矩阵
算法·leetcode·职场和发展
哦吼!1 小时前
算法基础-枚举
算法
重生之我要成为代码大佬1 小时前
从零讲透DFS-深度优先搜索-2(排序与组合)
开发语言·python·算法·深度优先遍历
uhakadotcom1 小时前
快速理解 tiktoken:OpenAI 模型的高效 BPE 分词器
算法·面试·github
鲸鱼宝宝成长记1 小时前
[蓝桥杯青少年组省赛 2024] 通关游戏的最少能量值
数据结构·算法
upward_growth1 小时前
一叶障目 -- 你不知道的"最长递增子序列"!
前端·javascript·算法
半间烟雨1 小时前
Web3(阶段一:入门)——哈希算法
算法·web3·区块链·哈希算法