LeetCode 周赛上分之旅 #34 按部就班地解决动态规划问题

双周赛 109

T1. 检查数组是否是好的(Easy)

  • 标签:模拟、排序

T2. 将字符串中的元音字母排序(Medium)

  • 标签:模拟、排序

T3. 访问数组中的位置使分数最大(Medium)

  • 标签:动态规划

T4. 将一个数字表示成幂的和的方案数(Medium)

  • 标签:动态规划、01 背包

T1. 检查数组是否是好的(Easy)

https://leetcode.cn/problems/check-if-array-is-good/

题解(模拟)

简单模拟题。

先排序后依次验证,最后验证尾数 N。

class Solution {
public:
    bool isGood(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        for (int i = 0; i < n - 1; i++) {
            if (i + 1 != nums[i]) return false;
        }
        return nums[n - 1] == n - 1;
    }
};
class Solution:
    def isGood(self, nums: List[int]) -> bool:
        return sorted(nums) == (list(range(1, len(nums))) + [len(nums) - 1])

复杂度分析:

  • 时间复杂度:O(nlgn) 瓶颈在排序;
  • 空间复杂度:O(lgn) 排序递归栈空间。

T2. 将字符串中的元音字母排序(Medium)

https://leetcode.cn/problems/sort-vowels-in-a-string/

题解(模拟 + 排序)

先抽取元音字母排序,再填充到结果数组中,如果使用桶排序,可以优化时间复杂度到 O(n)。

class Solution {
public:
    string sortVowels(string s) {
        unordered_set<char>st{'a','e','i','o','u','A','E','I','O','U'};
        string temp = "";
        for(char c : s){
            if(st.count(c)) temp += c;
        }
        // 排序
        sort(temp.begin(), temp.end());
        // 输出
        int i = 0;
        for(char &c : s){ // 原地修改
            if(st.count(c)) c = temp[i++];
        }
        return s;
    }
};

桶排序:

class Solution {
public:
    string sortVowels(string s) {
        unordered_set<char> st {'a','e','i','o','u','A','E','I','O','U'};
        // 桶排序(有序字典)
        map<char, int> vowelCounts;
        for (char c : s) {
            if (st.count(c)) {
                vowelCounts[c]++;
            }
        }
        // 输出
        for (char& c : s) { // 原地修改
            if (st.count(c)) {
                c = vowelCounts.begin()->first;
                if (--vowelCounts.begin()->second == 0) {
                    vowelCounts.erase(vowelCounts.begin());
                } 
            }
        }
        return s;
    }
};

复杂度分析:

  • 时间复杂度:O(nlgn) 瓶颈在排序,桶排序可以优化到 O(n)
  • 空间复杂度:O(n) 临时字符串空间。

T3. 访问数组中的位置使分数最大(Medium)

https://leetcode.cn/problems/visit-array-positions-to-maximize-score/

题解(动态规划)

比较明显的动态规划问题,定义 dp[i][j] 表示到 [i] 为止的最大序列和,其中:

  • dp[i][0] 表示尾数为偶数的序列
  • dp[i][1] 表示尾数为奇数的序列

那么对于 nums[i] 来说:

  • 如果 nums[i] 是偶数,那么它可以无成本地添加到尾数为偶数的序列 dp[i - 1][0] 中,也可以在消耗成本 x 的情况下添加到尾数为奇数的序列 dp[i - 1][1] 中;
  • 如果 nums[i] 是奇数,那么它可以无成本地添加到尾数为奇数的序列 dp[i - 1][0] 中,也可以在消耗成本 x 的情况下添加到尾数为偶数的序列 dp[i - 1][1] 中。

于是有:

  • dp[i][0] = Math.max(dp[i - 1][0] + nums[i], dp[i - 1][1] + nums[i] - x)
  • dp[i][1] = Math.max(dp[i - 1][1] + nums[i], dp[i - 1][0] + nums[i] - x)

另外,由于题目要求起始点必须从 nums[0] 开始,于是我们区分两种初始状态:

  • 如果 nums[0] 为偶数,初始状态 dp[0][0] = nums[i],dp[0][1] = -INF,此处 INF 表示无效,在首次选择奇数时一定会选择从 dp[i - 1][0] 转移的分支,确保从 nums[0] 开始;
  • 如果 nums[0] 为奇数,初始状态 dp[0][0] = -INF,dp[0][1] = nums[0],此处 INF 表示无效,在首次选择偶数时一定会选择从 dp[i - 1][1] 转移的分支,确保从 nums[0] 开始;

最后,由于每次迭代只关心 i - 1 层子状态,可以使用滚动数组优化空间。

class Solution {
    fun maxScore(nums: IntArray, x: Int): Long {
        val INF = -0x3F3F3F3FL // 减少判断
        val n = nums.size
        var ret = nums[0].toLong()
        // 初始状态
        val dp = if (nums[0] % 2 == 0) {
            longArrayOf(nums[0].toLong(), INF)
        } else {
            longArrayOf(INF, nums[0].toLong())
        }
        // dp[i] 表示到 [i] 为止的最大序列和
        for (i in 1 until n) {
            if (nums[i] % 2 == 0) {
                // 偶数
                dp[0] = Math.max(dp[0] + nums[i], dp[1] + nums[i] - x)
            } else {
                // 奇数
                dp[1] = Math.max(dp[1] + nums[i], dp[0] + nums[i] - x)
            }
        }
        return Math.max(dp[0], dp[1])
    }
}

复杂度分析:

  • 时间复杂度:O(n) 线性遍历;
  • 空间复杂度:O(1) 仅使用常量级别空间。

T4. 将一个数字表示成幂的和的方案数(Medium)

https://leetcode.cn/problems/ways-to-express-an-integer-as-sum-of-powers/

题解(01 背包)

原问题等价于:求在体积为 n 的背包中可以选择的方案数

由于题目要求方案中的数字不能存在重复数,例如 [1, 1, 1] 的方案是非法的,所以每个数最多只能选择一次,即只有选和不选两个状态,这容易联想到 01 背包模型。

  • 物品:数字
  • 物品体积:数字对 x 的幂
  • 背包大小:n

令 dp[i][j] 表示枚举到 [i] 为止且选择体积为 j 的方案数,则对于 i 来说有 2 个选择:

  • 不选:dp[i][j] = dp[i - 1][j]
  • 选:dp[i][j] = dp[i - 1][j - nums[i]^m]
class Solution {
    fun numberOfWays(n: Int, x: Int): Int {
        val MOD = 1000000007
        // 预处理备选数
        val nums = LinkedList<Int>()
        var i = 1
        while (true) {
            val e = Math.pow(1.0 * i, 1.0 * x).toInt()
            if (e > n) break
            nums.add(e)
            i++
        }
        val m = nums.size
        // 01 背包 dp[i][j] 表示枚举到 [i] 为止且选择体积为 j 的方案数
        val dp = Array(m + 1) { IntArray(n + 1) }
        // 体积为 0 的方案数为 1
        for (i in 0 .. m) dp[i][0] = 1
        // 枚举物品
        for (i in 1 .. m) {
            for (j in 1 .. n) {
                // 不选
                dp[i][j] = dp[i - 1][j]
                // 选
                if (j >= nums[i - 1]) dp[i][j] = (dp[i][j] + dp[i - 1][j - nums[i - 1]]) % MOD
            }
        }
        return dp[m][n] // 枚举到末尾且选择体积为 n 的方案数
    }
}

取消物品维度优化空间复杂度:

class Solution {
    fun numberOfWays(n: Int, x: Int): Int {
        ...
        val m = nums.size
        // 01 背包 dp[i][j] 表示枚举到 [i] 为止且选择体积为 j 的方案数
        val dp = IntArray(n + 1)
        // 体积为 0 的方案数为 1
        dp[0] = 1
        // 枚举物品
        for (i in 1 .. m) {
            for (j in n downTo nums[i - 1]) { // 逆序(会使用子问题)
                dp[j] = (dp[j] + dp[j - nums[i - 1]]) % MOD
            }
        }
        return dp[n] // 枚举到末尾且选择体积为 n 的方案数
    }
}

在预处理的过程直接进行背包逻辑也可以:

class Solution {
    fun numberOfWays(n: Int, x: Int): Int {
        val MOD = 1000000007
        val dp = IntArray(n + 1)
        dp[0] = 1
        // 枚举物品
        var i = 1
        while (true) {
            val e = Math.pow(1.0 * i, 1.0 * x).toInt()
            if (e > n) break
            for (j in n downTo e) { // 逆序(会使用子问题)
                dp[j] = (dp[j] + dp[j - e]) % MOD
            }
            i++
        }
        return dp[n] // 枚举到末尾且选择体积为 n 的方案数
    }
}

复杂度分析:

  • 时间复杂度:O(nm) 其中 m 为 ^x\sqrt{n},动态规划节点时间复杂度为 O(nm)
  • 空间复杂度:O(n) DP 数组空间。

推荐阅读

LeetCode 上分之旅系列往期回顾:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容