LeetCode790~800题目解析

3 阅读12分钟

LeetCode 790-800. 题解合集


790. Domino and Tromino Tiling(多米诺和托米诺平铺)

题目描述

有两种形状的瓷砖:一种是 2 x 1 的多米诺形,另一种是形如 "L" 的托米诺形。两种形状都可以旋转。
给定整数 n,返回可以平铺 2 x n 的面板的方法的数量。返回对 10^9 + 7 取模的值。
平铺指的是每个正方形都必须有瓷砖覆盖。两个平铺不同,当且仅当面板上有四个方向上的
相邻单元中的两个,使得恰好有一个平铺有一个瓷砖占据两个正方形。

示例 1

  • 输入:n = 3
  • 输出:5

示例 2

  • 输入:n = 1
  • 输出:1
    提示:
  • 1 <= n <= 1000

解题思路

标准转移递推:设 f[n] 为恰好铺满 2 x n 的方案数,p[n] 为 2 x n 中有一个格子
凸出一格(即铺了 2 x (n-1) 再加上一个竖着的角)的"缺角"状态数量。
推导可得 f[n] = f[n-1] + f[n-2] + 2p[n-2],而 p 与 f 满足 p[n] = f[n-1] + p[n-1],
化简后得到经典递推 f[n] = 2
f[n-1] + f[n-3](n >= 3),初值 f[0]=1, f[1]=1, f[2]=2。
解法一(递推滚动,主提交):按 f[n] = 2*f[n-1] + f[n-3] 滚动三个值。
解法二(矩阵快速幂):把递推写成矩阵形式 [f[n], f[n-1], f[n-2]]^T =
M * [f[n-1], f[n-2], f[n-3]]^T,M = [[2,0,1],[1,0,0],[0,1,0]],用矩阵快速幂
在 O(log n) 内求出 f[n],适合 n 更大(如 10^9)的场景。

代码实现

class Solution {

    private static final int MOD = 1_000_000_007;

    // 解法一:递推 f[n] = 2*f[n-1] + f[n-3]
    public int numTilings(int n) {
        if (n == 1) {
            return 1;
        }
        if (n == 2) {
            return 2;
        }
        long f0 = 1; // f[0]
        long f1 = 1; // f[1]
        long f2 = 2; // f[2]
        for (int i = 3; i <= n; ++i) {
            long f3 = (2 * f2 + f0) % MOD;
            f0 = f1;
            f1 = f2;
            f2 = f3;
        }
        return (int) f2;
    }

    // 解法二:矩阵快速幂
    public int numTilings2(int n) {
        if (n == 1) {
            return 1;
        }
        if (n == 2) {
            return 2;
        }
        // M = [[2,0,1],[1,0,0],[0,1,0]]
        long[][] m = {{2, 0, 1}, {1, 0, 0}, {0, 1, 0}};
        long[][] p = pow(m, n - 2);
        // [f2, f1, f0]^T = [2, 1, 1]^T
        long f2 = 2, f1 = 1, f0 = 1;
        long fn2 = (p[0][0] * f2 + p[0][1] * f1 + p[0][2] * f0) % MOD;
        return (int) fn2;
    }

    private long[][] pow(long[][] base, int e) {
        long[][] res = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
        while (e > 0) {
            if ((e & 1) == 1) {
                res = mul(res, base);
            }
            base = mul(base, base);
            e >>= 1;
        }
        return res;
    }

    private long[][] mul(long[][] a, long[][] b) {
        long[][] c = new long[3][3];
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                long s = 0;
                for (int k = 0; k < 3; ++k) {
                    s = (s + a[i][k] * b[k][j]) % MOD;
                }
                c[i][j] = s;
            }
        }
        return c;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numTilings(1) == 1) ++pass; else System.out.println("FAIL 790-1");
        ++total; if (sol.numTilings(2) == 2) ++pass; else System.out.println("FAIL 790-2");
        ++total; if (sol.numTilings(3) == 5) ++pass; else System.out.println("FAIL 790-3");
        ++total; if (sol.numTilings(4) == 11) ++pass; else System.out.println("FAIL 790-4");
        ++total; if (sol.numTilings(5) == 24) ++pass; else System.out.println("FAIL 790-5");
        ++total; if (sol.numTilings(1000) == 979232805) ++pass; else System.out.println("FAIL 790-6");

        ++total; if (sol.numTilings2(1) == 1) ++pass; else System.out.println("FAIL 790-7");
        ++total; if (sol.numTilings2(2) == 2) ++pass; else System.out.println("FAIL 790-8");
        ++total; if (sol.numTilings2(3) == 5) ++pass; else System.out.println("FAIL 790-9");
        ++total; if (sol.numTilings2(4) == 11) ++pass; else System.out.println("FAIL 790-10");
        ++total; if (sol.numTilings2(5) == 24) ++pass; else System.out.println("FAIL 790-11");
        ++total; if (sol.numTilings2(1000) == 979232805) ++pass; else System.out.println("FAIL 790-12");

        if (pass == total) {
            System.out.println("== 0790 PASS ==");
        } else {
            System.out.println("== 0790 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(log n)O(1)

791. Custom Sort String(自定义字符串排序)

题目描述

给定两个字符串 order 和 s。order 的所有字母都是唯一的,并且预先按照一些自定义的顺序排序。
对 s 的字符进行置换,使其与排序的 order 相匹配。更具体地说,如果在 order 中的字符 x
出现字符 y 之前,那么在排列后的字符串中,x 也应该出现在 y 之前。
返回满足这个性质的 s 的任意一种排列。

示例 1

  • 输入:order = "cba", s = "abcd"
  • 输出:"cbad"

示例 2

  • 输入:order = "bcafg", s = "abcd"
  • 输出:"bcad"
    提示:
  • 1 <= order.length <= 26
  • 1 <= s.length <= 200
  • order 和 s 由小写英文字母组成
  • order 中的所有字符都不同

解题思路

按 order 中字符的优先级排列:可以给字符一个 rank(在 order 中的下标),未出现字符
排最后且任意顺序。
解法一(桶计数按优先级输出,主提交):先统计 s 中每个字母出现次数,然后按 order 顺序
输出各字母剩余次数,最后按字母表输出 order 中未出现的字母。
解法二(自定义比较器排序):把 s 转成字符数组,定义比较器按 rank 升序排;rank 缺失的
字符记为 26(次序无所谓)。适用面广、代码直观。

代码实现

class Solution {

    // 解法一:桶计数按序输出
    public String customSortString(String order, String s) {
        int[] cnt = new int[26];
        for (int i = 0; i < s.length(); ++i) {
            cnt[s.charAt(i) - 'a']++;
        }
        StringBuilder sb = new StringBuilder();
        // 1. 按 order 优先级输出
        for (int i = 0; i < order.length(); ++i) {
            int c = order.charAt(i) - 'a';
            while (cnt[c]-- > 0) {
                sb.append((char) ('a' + c));
            }
        }
        // 2. order 中没有的字母按字母表顺序收尾
        for (int c = 0; c < 26; ++c) {
            while (cnt[c]-- > 0) {
                sb.append((char) ('a' + c));
            }
        }
        return sb.toString();
    }

    // 解法二:自定义比较器
    public String customSortString2(String order, String s) {
        int[] rank = new int[26];
        java.util.Arrays.fill(rank, 26);
        for (int i = 0; i < order.length(); ++i) {
            rank[order.charAt(i) - 'a'] = i;
        }
        Character[] arr = new Character[s.length()];
        for (int i = 0; i < s.length(); ++i) {
            arr[i] = s.charAt(i);
        }
        java.util.Arrays.sort(arr, (a, b) -> rank[a - 'a'] - rank[b - 'a']);
        StringBuilder sb = new StringBuilder();
        for (char c : arr) {
            sb.append(c);
        }
        return sb.toString();
    }

    // 校验:str 中属于 order 的字符按其出现顺序排列
    private static boolean orderedBy(String str, String order) {
        int[] pos = new int[26];
        java.util.Arrays.fill(pos, -1);
        for (int i = 0; i < order.length(); ++i) {
            pos[order.charAt(i) - 'a'] = i;
        }
        int last = -1;
        for (int i = 0; i < str.length(); ++i) {
            int p = pos[str.charAt(i) - 'a'];
            if (p >= 0) {
                if (p < last) {
                    return false;
                }
                last = p;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.customSortString("cba", "abcd").equals("cbad")) ++pass; else System.out.println("FAIL 791-1 " + sol.customSortString("cba", "abcd"));
        ++total; if (sol.customSortString("bcafg", "abcd").equals("bcad")) ++pass; else System.out.println("FAIL 791-2 " + sol.customSortString("bcafg", "abcd"));
        ++total; if (sol.customSortString("kqep", "pekeq").equals("kqeep")) ++pass; else System.out.println("FAIL 791-3 " + sol.customSortString("kqep", "pekeq"));
        String lstr = "utzoampdgkalexslxoqfkdjoczajxtuhqyxvlfatmptqdsochtdzgypsfkgqwbgqbcamdqnqzptqhshxrxtdhzjkbbufmqknineouacewfnhhpx";
        ++total; if (sol.customSortString("hucw", lstr).length() == 111) ++pass; else System.out.println("FAIL 791-4");
        // 验证长串按 order 顺序排列(order = "hucw")
        String longOrder = sol.customSortString("hucw", lstr);
        ++total; if (longOrder.length() == 111 && orderedBy(longOrder, "hucw")) ++pass; else System.out.println("FAIL 791-5");

        ++total; if (sol.customSortString2("cba", "abcd").equals("cbad")) ++pass; else System.out.println("FAIL 791-6");
        ++total; if (sol.customSortString2("kqep", "pekeq").equals("kqeep")) ++pass; else System.out.println("FAIL 791-7");

        if (pass == total) {
            System.out.println("== 0791 PASS ==");
        } else {
            System.out.println("== 0791 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n + m + 26)O(26)
解法二O(n log n)O(n)

792. Number of Matching Subsequences(匹配子序列的单词数)

题目描述

给定字符串 s 和字符串数组 words,返回 words[i] 中是 s 的子序列的单词个数。
字符串的子序列是从原始字符串中生成的新字符串,可以从中删去一些字符(可以是 none),
而不改变其余字符的相对顺序。
例如,"ace" 是 "abcde" 的子序列。

示例 1

  • 输入:s = "abcde", words = ["a","bb","acd","ace"]
  • 输出:3
  • 解释:有三个是 s 的子序列的单词: "a", "acd", "ace"。

示例 2

  • 输入:s = "dsahjpjauf", words = ["ahjpjau","ja","ahbwzgqnuk","tnmlanowax"]
  • 输出:2
    提示:
  • 1 <= s.length <= 5 * 10^4
  • 1 <= words.length <= 5000
  • 1 <= words[i].length <= 50
  • words[i] 和 s 都只由小写字母组成

解题思路

朴素做法对每个单词用双指针扫 s,最坏 O(words * s) 会超时。利用 s 的 26 个字母位置表,
每次用二分定位"字符首次出现于 pos 及之后",总复杂度 O(sum(len(words)) * log n)。
解法一(位置表 + 二分,主提交):预存每个字母在 s 中的所有下标,匹配单词时维护指针 pos,
对每个字符二分找 >= pos 的最小下标,找不到即失败。
解法二(多指针分桶推进):为 26 个字母各自维护待匹配的单词队列(每个单词存当前已匹配进度),
扫描 s 一遇到字母 c,就把 c 队列中所有单词的进度 +1,进度到头的单词计数 +1。
时间 O(len(s) + total),是另一经典做法。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:位置表 + 二分
    public int numMatchingSubseq(String s, String[] words) {
        List<Integer>[] pos = new List[26];
        for (int c = 0; c < 26; ++c) {
            pos[c] = new ArrayList<>();
        }
        for (int i = 0; i < s.length(); ++i) {
            pos[s.charAt(i) - 'a'].add(i);
        }
        int ans = 0;
        for (String w : words) {
            if (isSubseq(w, pos)) {
                ++ans;
            }
        }
        return ans;
    }

    private boolean isSubseq(String w, List<Integer>[] pos) {
        int cur = -1;
        for (int i = 0; i < w.length(); ++i) {
            List<Integer> list = pos[w.charAt(i) - 'a'];
            int idx = lowerBound(list, cur + 1);
            if (idx == list.size()) {
                return false;
            }
            cur = list.get(idx);
        }
        return true;
    }

    private int lowerBound(List<Integer> list, int target) {
        int lo = 0, hi = list.size();
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (list.get(mid) < target) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    // 解法二:多指针分桶推进
    public int numMatchingSubseq2(String s, String[] words) {
        List<Node>[] buckets = new List[26];
        for (int c = 0; c < 26; ++c) {
            buckets[c] = new ArrayList<>();
        }
        for (String w : words) {
            buckets[w.charAt(0) - 'a'].add(new Node(w, 0));
        }
        int ans = 0;
        for (int i = 0; i < s.length(); ++i) {
            int c = s.charAt(i) - 'a';
            List<Node> waiting = buckets[c];
            buckets[c] = new ArrayList<>();
            for (Node node : waiting) {
                node.idx++;
                if (node.idx == node.word.length()) {
                    ++ans;
                } else {
                    buckets[node.word.charAt(node.idx) - 'a'].add(node);
                }
            }
        }
        return ans;
    }

    private static class Node {
        String word;
        int idx;
        Node(String word, int idx) {
            this.word = word;
            this.idx = idx;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numMatchingSubseq("abcde", new String[] {"a", "bb", "acd", "ace"}) == 3) ++pass; else System.out.println("FAIL 792-1");
        ++total; if (sol.numMatchingSubseq("dsahjpjauf", new String[] {"ahjpjau", "ja", "ahbwzgqnuk", "tnmlanowax"}) == 2) ++pass; else System.out.println("FAIL 792-2");
        ++total; if (sol.numMatchingSubseq("aaaa", new String[] {"a", "aa", "aaa", "aaaa", "aaaaa"}) == 4) ++pass; else System.out.println("FAIL 792-3");

        ++total; if (sol.numMatchingSubseq2("abcde", new String[] {"a", "bb", "acd", "ace"}) == 3) ++pass; else System.out.println("FAIL 792-4");
        ++total; if (sol.numMatchingSubseq2("dsahjpjauf", new String[] {"ahjpjau", "ja", "ahbwzgqnuk", "tnmlanowax"}) == 2) ++pass; else System.out.println("FAIL 792-5");
        ++total; if (sol.numMatchingSubseq2("aaaa", new String[] {"a", "aa", "aaa", "aaaa", "aaaaa"}) == 4) ++pass; else System.out.println("FAIL 792-6");

        if (pass == total) {
            System.out.println("== 0792 PASS ==");
        } else {
            System.out.println("== 0792 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(∑w* log n)O(n)
解法二O(n + ∑w)O(∑w)

793. Preimage Size of Factorial Zeroes Function(阶乘函数后K个零)

题目描述

f(x) 是 x! 末尾是 0 的数量。回想一下 x! = 1 * 2 * 3 * ... * x,且 0! = 1。
例如,f(3) = 0,因为 3! = 6 的末尾没有 0;而 f(11) = 2,因为 11! 末端有 2 个 0。
给定 k,找出返回能满足 f(x) = k 的非负整数 x 的数量。

示例 1

  • 输入:k = 0
  • 输出:5
  • 解释:0!, 1!, 2!, 3!, 和 4! 均符合 k = 0 的条件。

示例 2

  • 输入:k = 5
  • 输出:0
  • 解释:没有匹配到这样的 x!,符合 k = 5 的条件。

示例 3

  • 输入:k = 3
  • 输出:5
    提示:
  • 0 <= k <= 10^9

解题思路

关键事实:f(x) 是 x 的单调不减函数,且满足 f(x) = k 的 x 的个数要么是 0,要么是 5。
原因:x! 末尾 0 的个数由因子 5 的个数决定;每跨过一个含因子 5 的窗口,f 值跳跃 +1,
而在两个"5 的倍数"之间 f 值不变,正好有 5 个整数共享同一个 f 值。
解法一(二分 + 几何计数,主提交):若存在 y 使 f(y) = k,则答案必为 5;否则 0。
用二分找 f(x) >= k 的最小 x,验证 f(x) == k。注意 k 很大,上界取 5*k + 5 即可
(f(5k) >= k,且足够大)。
解法二(直接判断 k 落在哪段):f 函数序列在 5 的窗口内保持不变,可用
count(x) = x/5 + x/25 + ... 求 f;对给定的 k,找最小的 x 使 f(x) >= k,再验证相等。

代码实现

class Solution {

    // 解法一:二分找最小 x,验证 f(x) == k
    public int preimageSizeFZF(int k) {
        long lo = 0, hi = 5L * (k + 1); // 上界足够大:f(5k) >= k
        while (lo < hi) {
            long mid = (lo + hi) >>> 1;
            if (trailingZeros(mid) >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return trailingZeros(lo) == k ? 5 : 0;
    }

    private long trailingZeros(long x) {
        long cnt = 0;
        while (x > 0) {
            x /= 5;
            cnt += x;
        }
        return cnt;
    }

    // 解法二:几何计数(等值段判断)
    public int preimageSizeFZF2(int k) {
        // 找到最小 x 满足 f(x) >= k
        long x = 0;
        for (long step = 5L * (k + 1); step > 0; step >>= 1) {
            while (trailingZeros(x + step) < k) {
                x += step;
            }
        }
        // 现在 trailingZeros(x) < k,trailingZeros(x+1) >= k
        return trailingZeros(x + 1) == k ? 5 : 0;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.preimageSizeFZF(0) == 5) ++pass; else System.out.println("FAIL 793-1");
        ++total; if (sol.preimageSizeFZF(5) == 0) ++pass; else System.out.println("FAIL 793-2");
        ++total; if (sol.preimageSizeFZF(3) == 5) ++pass; else System.out.println("FAIL 793-3");
        ++total; if (sol.preimageSizeFZF(1) == 5) ++pass; else System.out.println("FAIL 793-4");
        ++total; if (sol.preimageSizeFZF(7) == 5) ++pass; else System.out.println("FAIL 793-5");
        ++total; if (sol.preimageSizeFZF(1000000000) == 5) ++pass; else System.out.println("FAIL 793-6");

        ++total; if (sol.preimageSizeFZF2(0) == 5) ++pass; else System.out.println("FAIL 793-7");
        ++total; if (sol.preimageSizeFZF2(5) == 0) ++pass; else System.out.println("FAIL 793-8");
        ++total; if (sol.preimageSizeFZF2(3) == 5) ++pass; else System.out.println("FAIL 793-9");
        ++total; if (sol.preimageSizeFZF2(7) == 5) ++pass; else System.out.println("FAIL 793-10");

        if (pass == total) {
            System.out.println("== 0793 PASS ==");
        } else {
            System.out.println("== 0793 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

| 解法 | 时间复杂度 | 空间复杂度 |
| --- | | --- |
| 解法一 | O(log k * log x) | O(1) |
| 解法二 | O(log x) | O(1) |


794. Valid Tic-Tac-Toe State(有效的井字游戏)

题目描述

给你一个字符串数组 board 表示井字游戏的棋盘。当且仅当在井字游戏过程中,棋盘有可能
达到 board 所显示的状态时,才返回 true。
井字游戏的棋盘是一个 3 x 3 数组,由字符 ' '、'X' 和 'O' 组成。字符 ' ' 代表一个空位。
以下是井字游戏的规则:

  • 玩家轮流将字符放入空位(' ')中。
  • 玩家 1 总是放字符 'X',而玩家 2 总是放字符 'O'。
  • 'X' 和 'O' 只允许放置在空位中,不允许对已放有字符的位置进行填充。
  • 当有 3 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。
  • 当所有位置非空时,也算为游戏结束。
  • 如果游戏结束,玩家不允许再放置字符。

示例 1

  • 输入:board = ["O "," "," "]
  • 输出:false

示例 2

  • 输入:board = ["XOX"," X "," "]
  • 输出:false

示例 3

  • 输入:board = ["XOX","O O","XOX"]
  • 输出:true
    提示:
  • board.length == 3
  • board[i].length == 3
  • board[i][j] 是 'X'、'O' 或 ' '

解题思路

合法终局必须满足的条件:

  1. X 的个数等于 O 的个数或者多 1(X 先手)。
  2. 如果 X 已获胜,则 X 的个数必须比 O 多 1(获胜后对方不再落子);
    如果 O 已获胜,则 X 的个数必须等于 O 的个数。
  3. X 和 O 不能同时获胜(3x3 里实际上同时胜利不可能,但规则判断涵盖)。
    解法一(规则判定,主提交):统计 X/O 数量与胜负状态,套用上述判定。
    解法二(全局面模拟对照):枚举所有合法落子序列(回溯),看是否能恰好得到 board。
    规模小(9 个位置),可完整回溯,作为正确性对照。

代码实现

class Solution {

    // 解法一:规则判定
    public boolean validTicTacToe(String[] board) {
        int xCnt = 0, oCnt = 0;
        for (String row : board) {
            for (char c : row.toCharArray()) {
                if (c == 'X') {
                    ++xCnt;
                } else if (c == 'O') {
                    ++oCnt;
                }
            }
        }
        if (xCnt != oCnt && xCnt != oCnt + 1) {
            return false;
        }
        boolean xWin = win(board, 'X');
        boolean oWin = win(board, 'O');
        if (xWin && oWin) {
            return false;
        }
        if (xWin && xCnt != oCnt + 1) {
            return false;
        }
        if (oWin && xCnt != oCnt) {
            return false;
        }
        return true;
    }

    private boolean win(String[] board, char p) {
        for (int i = 0; i < 3; ++i) {
            // 行
            if (board[i].charAt(0) == p && board[i].charAt(1) == p && board[i].charAt(2) == p) {
                return true;
            }
            // 列
            if (board[0].charAt(i) == p && board[1].charAt(i) == p && board[2].charAt(i) == p) {
                return true;
            }
        }
        // 对角线
        return (board[0].charAt(0) == p && board[1].charAt(1) == p && board[2].charAt(2) == p)
            || (board[0].charAt(2) == p && board[1].charAt(1) == p && board[2].charAt(0) == p);
    }

    // 解法二:回溯全局面模拟(对照)
    public boolean validTicTacToe2(String[] board) {
        char[][] target = new char[3][3];
        char[][] cur = new char[3][3];
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                target[i][j] = board[i].charAt(j);
                cur[i][j] = ' ';
            }
        }
        return play(cur, target, 'X');
    }

    private boolean play(char[][] cur, char[][] target, char turn) {
        // 当前状态是否与 target 相同(到达即成功,不需要再走)
        if (same(cur, target)) {
            return true;
        }
        if (hasWin(cur)) {
            return false; // 已决出胜负,不能再走
        }
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                if (target[i][j] == ' ') {
                    continue; // 目标为空而我们放子则永远到不了
                }
                if (cur[i][j] == ' ' && target[i][j] == turn) {
                    cur[i][j] = turn;
                    if (play(cur, target, turn == 'X' ? 'O' : 'X')) {
                        cur[i][j] = ' ';
                        return true;
                    }
                    cur[i][j] = ' ';
                }
            }
        }
        return false;
    }

    private boolean same(char[][] a, char[][] b) {
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                if (a[i][j] != b[i][j]) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean hasWin(char[][] g) {
        for (int i = 0; i < 3; ++i) {
            if (g[i][0] != ' ' && g[i][0] == g[i][1] && g[i][1] == g[i][2]) {
                return true;
            }
            if (g[0][i] != ' ' && g[0][i] == g[1][i] && g[1][i] == g[2][i]) {
                return true;
            }
        }
        return (g[0][0] != ' ' && g[0][0] == g[1][1] && g[1][1] == g[2][2])
            || (g[0][2] != ' ' && g[0][2] == g[1][1] && g[1][1] == g[2][0]);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        String[] b1 = {"O  ", "   ", "   "};
        ++total; if (!sol.validTicTacToe(b1)) ++pass; else System.out.println("FAIL 794-1");
        String[] b2 = {"XOX", " X ", "   "};
        ++total; if (!sol.validTicTacToe(b2)) ++pass; else System.out.println("FAIL 794-2");
        String[] b3 = {"XOX", "O O", "XOX"};
        ++total; if (sol.validTicTacToe(b3)) ++pass; else System.out.println("FAIL 794-3");
        String[] b4 = {"XXX", "   ", "OOO"};
        ++total; if (!sol.validTicTacToe(b4)) ++pass; else System.out.println("FAIL 794-4");
        String[] b5 = {"XXX", "OOX", "OOX"};
        ++total; if (sol.validTicTacToe(b5)) ++pass; else System.out.println("FAIL 794-5");
        String[] b6 = {"XXX", "OOO", "   "};
        ++total; if (!sol.validTicTacToe(b6)) ++pass; else System.out.println("FAIL 794-6");

        String[] c1 = {"XOX", "OXO", "XOX"};
        ++total; if (sol.validTicTacToe2(c1)) ++pass; else System.out.println("FAIL 794-7");
        String[] c2 = {"O  ", "   ", "   "};
        ++total; if (!sol.validTicTacToe2(c2)) ++pass; else System.out.println("FAIL 794-8");
        String[] c3 = {"XOX", " X ", "   "};
        ++total; if (!sol.validTicTacToe2(c3)) ++pass; else System.out.println("FAIL 794-9");
        String[] c4 = {"XXX", "OOX", "OOX"};
        ++total; if (sol.validTicTacToe2(c4)) ++pass; else System.out.println("FAIL 794-10");

        if (pass == total) {
            System.out.println("== 0794 PASS ==");
        } else {
            System.out.println("== 0794 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)O(1)
解法二O(9!)(对照)O(9)

795. Number of Subarrays with Bounded Maximum(区间子数组个数)

题目描述

给你一个整数数组 nums 和两个整数:left 及 right。找出 nums 中连续、非空且其中
最大元素在范围 [left, right] 内的子数组,并返回满足条件的子数组的个数。
生成的测试用例保证结果符合 32-bit 整数范围。

示例 1

  • 输入:nums = [2,1,4,3], left = 2, right = 3
  • 输出:3
  • 解释:满足条件的三个子数组:[2], [2,1], [3]

示例 2

  • 输入:nums = [2,9,2,5,6], left = 2, right = 8
  • 输出:7
    提示:
  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9
  • 0 <= left <= right <= 10^9

解题思路

「最大值 <= right」的子数组个数减去「最大值 <= left-1」的子数组个数,即答案。
对上限 limit,统计最大值不超过 limit 的子数组数:扫描时维护连续「<= limit」段长 len,
每段贡献 len*(len+1)/2 个。
解法一(两次扫描差值,主提交):count(<=right) - count(<=left-1)。
解法二(一次扫描双指针):维护区间 [i, j],保证左界的上一个 > right 元素之后、
且从最近的 > right 元素后到当前位置;同时记录最近一个 >= left 元素的贡献。
具体:遍历右端点 j,遇到 > right 则 leftBound=j,遇到在 [left,right] 则 lastIn=j,
答案 += lastIn - leftBound(即以 j 结尾的合法子数组个数)。

代码实现

class Solution {

    // 解法一:<=right 减去 <=left-1
    public int numSubarrayBoundedMax(int[] nums, int left, int right) {
        return countLE(nums, right) - countLE(nums, left - 1);
    }

    private int countLE(int[] nums, int limit) {
        long ans = 0;
        long len = 0;
        for (int x : nums) {
            if (x <= limit) {
                ++len;
            } else {
                ans += len * (len + 1) / 2;
                len = 0;
            }
        }
        ans += len * (len + 1) / 2;
        return (int) ans;
    }

    // 解法二:一次扫描双指针
    public int numSubarrayBoundedMax2(int[] nums, int left, int right) {
        int ans = 0;
        int lastOut = -1;   // 最近一个 > right 的下标
        int lastIn = -1;    // 最近一个在 [left, right] 内的下标
        for (int j = 0; j < nums.length; ++j) {
            if (nums[j] > right) {
                lastOut = j;
            }
            if (nums[j] >= left && nums[j] <= right) {
                lastIn = j;
            }
            if (lastIn > lastOut) {
                ans += lastIn - lastOut;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.numSubarrayBoundedMax(new int[] {2, 1, 4, 3}, 2, 3) == 3) ++pass; else System.out.println("FAIL 795-1");
        ++total; if (sol.numSubarrayBoundedMax(new int[] {2, 9, 2, 5, 6}, 2, 8) == 7) ++pass; else System.out.println("FAIL 795-2");
        ++total; if (sol.numSubarrayBoundedMax(new int[] {1, 1, 1}, 0, 0) == 0) ++pass; else System.out.println("FAIL 795-3");
        ++total; if (sol.numSubarrayBoundedMax(new int[] {10, 5, 10}, 5, 7) == 1) ++pass; else System.out.println("FAIL 795-4");
        ++total; if (sol.numSubarrayBoundedMax(new int[] {16, 69, 88, 85, 99, 84, 44, 3, 44, 68}, 58, 100) == 48) ++pass; else System.out.println("FAIL 795-5");

        ++total; if (sol.numSubarrayBoundedMax2(new int[] {2, 1, 4, 3}, 2, 3) == 3) ++pass; else System.out.println("FAIL 795-6");
        ++total; if (sol.numSubarrayBoundedMax2(new int[] {2, 9, 2, 5, 6}, 2, 8) == 7) ++pass; else System.out.println("FAIL 795-7");
        ++total; if (sol.numSubarrayBoundedMax2(new int[] {10, 5, 10}, 5, 7) == 1) ++pass; else System.out.println("FAIL 795-8");
        ++total; if (sol.numSubarrayBoundedMax2(new int[] {16, 69, 88, 85, 99, 84, 44, 3, 44, 68}, 58, 100) == 48) ++pass; else System.out.println("FAIL 795-9");

        if (pass == total) {
            System.out.println("== 0795 PASS ==");
        } else {
            System.out.println("== 0795 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n)O(1)

796. Rotate String(旋转字符串)

题目描述

给定两个字符串 s 和 goal。如果在若干次旋转操作之后,s 能变成 goal,那么返回 true。
s 的旋转操作就是将 s 最左边的字符移动到最右边。
例如,若 s = 'abcde',在旋转一次之后结果就是 'bcdea'。

示例 1

  • 输入:s = "abcde", goal = "cdeab"
  • 输出:true

示例 2

  • 输入:s = "abcde", goal = "abced"
  • 输出:false
    提示:
  • 1 <= s.length, goal.length <= 100
  • s 和 goal 由小写英文字母组成

解题思路

s 的任何旋转结果都是 s + s 的子串(长度为 n)。因此当长度相等且 goal 是 s+s 的子串时,
goal 必为 s 的某个旋转。
解法一(拼接判定,主提交):s.length() == goal.length() 且 (s + s).contains(goal)。
解法二(逐次旋转模拟):与「同构字符串」思路一致,枚举旋转次数依次比较(对照实现,
逻辑直观,O(n^2))。

代码实现

class Solution {

    // 解法一:s+s 包含判定
    public boolean rotateString(String s, String goal) {
        return s.length() == goal.length() && (s + s).contains(goal);
    }

    // 解法二:逐次旋转模拟
    public boolean rotateString2(String s, String goal) {
        if (s.length() != goal.length()) {
            return false;
        }
        int n = s.length();
        for (int k = 0; k < n; ++k) {
            // 从 k 位开始旋转:s[k..n-1] + s[0..k-1]
            boolean ok = true;
            for (int i = 0; i < n; ++i) {
                if (s.charAt((k + i) % n) != goal.charAt(i)) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.rotateString("abcde", "cdeab")) ++pass; else System.out.println("FAIL 796-1");
        ++total; if (!sol.rotateString("abcde", "abced")) ++pass; else System.out.println("FAIL 796-2");
        ++total; if (sol.rotateString("aa", "aa")) ++pass; else System.out.println("FAIL 796-3");
        ++total; if (sol.rotateString("", "")) ++pass; else System.out.println("FAIL 796-4");
        ++total; if (sol.rotateString("abcabc", "bcabca")) ++pass; else System.out.println("FAIL 796-5");
        ++total; if (!sol.rotateString("abc", "abx")) ++pass; else System.out.println("FAIL 796-6");

        ++total; if (sol.rotateString2("abcde", "cdeab")) ++pass; else System.out.println("FAIL 796-7");
        ++total; if (!sol.rotateString2("abcde", "abced")) ++pass; else System.out.println("FAIL 796-8");
        ++total; if (sol.rotateString2("abcabc", "bcabca")) ++pass; else System.out.println("FAIL 796-9");

        if (pass == total) {
            System.out.println("== 0796 PASS ==");
        } else {
            System.out.println("== 0796 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)(KMP 匹配)O(n)(拼接串)
解法二O(n^2)O(1)

797. All Paths From Source to Target(所有可能的路径)

题目描述

给你一个有 n 个节点的有向无环图(DAG),请你找出从节点 0 到节点 n-1 的所有路径并输出
(不要求按特定顺序)。
graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]
存在一条有向边)。

示例 1

  • 输入:graph = [[1,2],[3],[3],[]]
  • 输出:[[0,1,3],[0,2,3]]
  • 解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3

示例 2

  • 输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
  • 输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
    提示:
  • n == graph.length
  • 2 <= n <= 15
  • 0 <= graph[i][j] < n
  • graph[i][j] != i(即不存在自环)
  • graph[i] 中的所有元素互不相同
  • 保证输入为有向无环图(DAG)

解题思路

DAG 无环,直接 DFS 回溯收集从 0 到 n-1 的所有路径,无需判重。
解法一(DFS 回溯,主提交):从 0 出发,path 记录当前路径,到达 n-1 时复制加入答案。
解法二(BFS 拆解路径):队列中存「从 0 到某点的路径」,逐层扩展,到达终点即收集;
也能得到全部路径,逻辑等价、写法稍绕。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    private List<List<Integer>> ans;
    private List<Integer> path;

    // 解法一:DFS 回溯
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        ans = new ArrayList<>();
        path = new ArrayList<>();
        path.add(0);
        dfs(0, graph);
        return ans;
    }

    private void dfs(int u, int[][] graph) {
        if (u == graph.length - 1) {
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int v : graph[u]) {
            path.add(v);
            dfs(v, graph);
            path.remove(path.size() - 1);
        }
    }

    // 解法二:BFS 拆解路径
    public List<List<Integer>> allPathsSourceTarget2(int[][] graph) {
        List<List<Integer>> res = new ArrayList<>();
        int n = graph.length;
        // 队列元素:从 0 到某点的路径
        java.util.Deque<List<Integer>> q = new java.util.ArrayDeque<>();
        List<Integer> start = new ArrayList<>();
        start.add(0);
        q.offer(start);
        while (!q.isEmpty()) {
            List<Integer> cur = q.poll();
            int last = cur.get(cur.size() - 1);
            if (last == n - 1) {
                res.add(cur);
                continue;
            }
            for (int v : graph[last]) {
                List<Integer> next = new ArrayList<>(cur);
                next.add(v);
                q.offer(next);
            }
        }
        return res;
    }

    private static boolean sameSetOfLists(List<List<Integer>> a, List<List<Integer>> b) {
        if (a.size() != b.size()) {
            return false;
        }
        java.util.Set<String> set = new java.util.HashSet<>();
        for (List<Integer> l : a) {
            set.add(l.toString());
        }
        for (List<Integer> l : b) {
            if (!set.contains(l.toString())) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        int[][] g1 = {{1, 2}, {3}, {3}, {}};
        List<List<Integer>> r1 = sol.allPathsSourceTarget(g1);
        ++total; if (r1.size() == 2 && r1.get(0).get(0) == 0 && r1.get(0).contains(1) && r1.get(0).contains(3)) ++pass; else System.out.println("FAIL 797-1 " + r1);

        int[][] g2 = {{4, 3, 1}, {3, 2, 4}, {3}, {4}, {}};
        List<List<Integer>> r2 = sol.allPathsSourceTarget(g2);
        ++total; if (r2.size() == 5) ++pass; else System.out.println("FAIL 797-2 " + r2);

        int[][] g3 = {{1}, {}};
        List<List<Integer>> r3 = sol.allPathsSourceTarget(g3);
        ++total; if (r3.size() == 1 && r3.get(0).get(1) == 1) ++pass; else System.out.println("FAIL 797-3 " + r3);

        List<List<Integer>> s1 = sol.allPathsSourceTarget2(g1);
        ++total; if (sameSetOfLists(s1, r1)) ++pass; else System.out.println("FAIL 797-4 " + s1);
        List<List<Integer>> s2 = sol.allPathsSourceTarget2(g2);
        ++total; if (sameSetOfLists(s2, r2)) ++pass; else System.out.println("FAIL 797-5 " + s2);
        List<List<Integer>> s3 = sol.allPathsSourceTarget2(g3);
        ++total; if (sameSetOfLists(s3, r3)) ++pass; else System.out.println("FAIL 797-6 " + s3);

        if (pass == total) {
            System.out.println("== 0797 PASS ==");
        } else {
            System.out.println("== 0797 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(2^n * n)O(n)(递归) + O(2^n * n)(答案)
解法二O(2^n * n)O(2^n * n)(队列存路径)

798. Smallest Rotation with Highest Score(得分最高的最小轮调)

题目描述

给你一个数组 nums,我们可以将它按一个非负整数 k 进行轮调,这样可以使数组变为
[nums[k], nums[k+1], ..., nums[nums.length-1], nums[0], nums[1], ..., nums[k-1]] 的形式。
此后,任何值小于或等于其索引的项都可以记作一分。
例如,数组为 nums = [2,4,1,3,0],我们按 k = 2 进行轮调后,它将变成 [1,3,0,2,4]。
这将记为 3 分(0<=2、2<=3、4<=4 各计 1 分)。
在所有可能的轮调中,返回我们所能得到的最高分数对应的轮调下标 k。如果有多个答案,
返回满足条件的最小的下标 k。

示例 1

  • 输入:nums = [2,3,1,4,0]
  • 输出:3

示例 2

  • 输入:nums = [1,3,0,2,4]
  • 输出:0
    提示:
  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] < nums.length

解题思路

轮调 k 后元素 nums[i] 位于新下标 (i - k + n) % n,要求 nums[i] <= (i - k) % n。
直接对每个 k 统计会 O(n^2)。经典差分做法:
固定元素 i(值 v = nums[i]),令 k 变化,计算使 v 得分的 k 集合。轮调 k 后元素位于
新下标 (i - k + n) % n,要求 v <= (i - k + n) % n。令下标 j = (i - k) mod n,
j 遍历 [v, n-1] 时 v 得分,此时 k ≡ (i - j) mod n,即 k 属于环形区间
[ (i+1) % n, (i - v + n) % n ](顺时针)。用差分数组标记每个元素的得分 k 集合,
最后扫描一遍取分数最大的最小 k。
解法一(差分数组,主提交):对每个 i 计算其得分 k 的环形区间并打差分,统计最大值对应最小 k。
解法二(事件点计数):与差分等价的计数实现(对照代码)。
解法三(暴力,仅小数据对照):O(n^2) 直接枚举每个 k 验证。

代码实现

class Solution {

    // 解法一:差分数组(环形区间)
    public int bestRotation(int[] nums) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            int v = nums[i];
            // k 属于环形区间 [kMin, kMax](顺时针),表示该元素在轮调 k 下得分
            int kMin = (i + 1) % n;
            int kMax = (i - v + n) % n;
            int len = (kMax - kMin + n) % n + 1;
            if (len == n) {
                diff[0]++;
                diff[n]--;
            } else if (kMin <= kMax) {
                diff[kMin]++;
                diff[kMax + 1]--;
            } else {
                // 环形跨过 0:拆成 [kMin, n-1] 与 [0, kMax]
                diff[kMin]++;
                diff[n]--;
                diff[0]++;
                diff[kMax + 1]--;
            }
        }
        int bestK = 0;
        int bestScore = -1;
        int cur = 0;
        for (int k = 0; k < n; ++k) {
            cur += diff[k];
            if (cur > bestScore) {
                bestScore = cur;
                bestK = k;
            }
        }
        return bestK;
    }

    // 解法二:事件点计数(环形拆段,与差分等价)
    public int bestRotation2(int[] nums) {
        int n = nums.length;
        int[] events = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            int v = nums[i];
            int kMin = (i + 1) % n;
            int kMax = (i - v + n) % n;
            int len = (kMax - kMin + n) % n + 1;
            if (len == n) {
                events[0]++;
                events[n]--;
            } else if (kMin <= kMax) {
                events[kMin]++;
                events[kMax + 1]--;
            } else {
                events[kMin]++;
                events[n]--;
                events[0]++;
                events[kMax + 1]--;
            }
        }
        int bestK = 0;
        int bestScore = -1;
        int cur = 0;
        for (int k = 0; k < n; ++k) {
            cur += events[k];
            if (cur > bestScore) {
                bestScore = cur;
                bestK = k;
            }
        }
        return bestK;
    }

    // 暴力对照:O(n^2) 验证小数据
    public int bestRotationBrute(int[] nums) {
        int n = nums.length;
        int bestK = 0, bestScore = -1;
        for (int k = 0; k < n; ++k) {
            int score = 0;
            for (int i = 0; i < n; ++i) {
                int newIdx = (i - k + n) % n;
                if (nums[i] <= newIdx) {
                    ++score;
                }
            }
            if (score > bestScore) {
                bestScore = score;
                bestK = k;
            }
        }
        return bestK;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.bestRotation(new int[] {2, 3, 1, 4, 0}) == 3) ++pass; else System.out.println("FAIL 798-1");
        ++total; if (sol.bestRotation(new int[] {1, 3, 0, 2, 4}) == 0) ++pass; else System.out.println("FAIL 798-2");
        ++total; if (sol.bestRotation(new int[] {0}) == 0) ++pass; else System.out.println("FAIL 798-3");
        ++total; if (sol.bestRotation(new int[] {0, 1, 2, 3, 4}) == 0) ++pass; else System.out.println("FAIL 798-4");
        ++total; if (sol.bestRotation(new int[] {4, 3, 2, 1, 0}) == 0) ++pass; else System.out.println("FAIL 798-5");

        ++total; if (sol.bestRotation2(new int[] {2, 3, 1, 4, 0}) == 3) ++pass; else System.out.println("FAIL 798-6");
        ++total; if (sol.bestRotation2(new int[] {1, 3, 0, 2, 4}) == 0) ++pass; else System.out.println("FAIL 798-7");
        ++total; if (sol.bestRotation2(new int[] {4, 3, 2, 1, 0}) == 0) ++pass; else System.out.println("FAIL 798-8");

        // 与暴力对照小随机
        java.util.Random rnd = new java.util.Random(42);
        boolean ok = true;
        for (int t = 0; t < 50; ++t) {
            int n = 1 + rnd.nextInt(6);
            int[] a = new int[n];
            for (int i = 0; i < n; ++i) {
                a[i] = rnd.nextInt(n);
            }
            int expect = sol.bestRotationBrute(a);
            int got = sol.bestRotation(a);
            if (expect != got) {
                ok = false;
                System.out.println("MISMATCH 798-rand expect=" + expect + " got=" + got + " arr=" + java.util.Arrays.toString(a));
                break;
            }
        }
        ++total; if (ok) ++pass; else System.out.println("FAIL 798-9");

        if (pass == total) {
            System.out.println("== 0798 PASS ==");
        } else {
            System.out.println("== 0798 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)
解法二O(n)O(n)

799. Champagne Tower(香槟塔)

题目描述

我们把玻璃杯摆成金字塔的形状,其中第一层有 1 个玻璃杯,第二层有 2 个,依次类推到第 100 层,
每个玻璃杯将盛有香槟。
从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的
流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。
(当最底层的玻璃杯满了,香槟会流到地板上)
例如,在倾倒一杯香槟后,最顶层的玻璃杯满了。倾倒了两杯香槟后,第二层的两个玻璃杯各自盛放
一半的香槟。在倒三杯香槟后,第二层的香槟满了。在倒第四杯后,第三层中间的玻璃杯盛放了一半
的香槟,他两边的玻璃杯各自盛放了四分之一的香槟。
现在当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例
(i 和 j 都从 0 开始)。

示例 1

  • 输入:poured = 1, query_glass = 1, query_row = 1
  • 输出:0.00000

示例 2

  • 输入:poured = 2, query_glass = 1, query_row = 1
  • 输出:0.50000

示例 3

  • 输入:poured = 100000009, query_row = 33, query_glass = 17
  • 输出:1.00000
    提示:
  • 0 <= poured <= 10^9
  • 0 <= query_glass <= query_row < 100

解题思路

逐层模拟:cur[i] 表示当前行第 i 个杯子的香槟量(可为溢出量),下一行某个杯子接收的量
等于上一行相邻杯子溢出量的一半。每行处理完把 >1 的部分作溢出量传给下一行。
解法一(滚动数组逐层递推,主提交):第 r 行下标 i 的杯子满后溢出 (cur[i]-1)/2,
分别加到下一行的 i 与 i+1。只保留当前行数组,滚动更新。
解法二(二维数组全模拟):用 dp[r][c] 保存每层每个杯子累计到达的香槟量,处理到 query_row
即可。空间 O(rows^2),思路最直白,作为对照实现。

代码实现

class Solution {

    // 解法一:滚动数组逐层递推
    public double champagneTower(int poured, int query_row, int query_glass) {
        double[] cur = new double[] {poured};
        for (int r = 0; r < query_row; ++r) {
            double[] next = new double[r + 2];
            for (int c = 0; c <= r; ++c) {
                if (cur[c] > 1) {
                    double overflow = (cur[c] - 1) / 2.0;
                    next[c] += overflow;
                    next[c + 1] += overflow;
                }
            }
            cur = next;
        }
        return Math.min(1.0, cur[Math.min(query_glass, cur.length - 1)]);
    }

    // 解法二:二维数组全模拟
    public double champagneTower2(int poured, int query_row, int query_glass) {
        double[][] dp = new double[query_row + 1][query_row + 1];
        dp[0][0] = poured;
        for (int r = 0; r < query_row; ++r) {
            for (int c = 0; c <= r; ++c) {
                if (dp[r][c] > 1) {
                    double overflow = (dp[r][c] - 1) / 2.0;
                    dp[r + 1][c] += overflow;
                    dp[r + 1][c + 1] += overflow;
                }
            }
        }
        return Math.min(1.0, dp[query_row][query_glass]);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (Math.abs(sol.champagneTower(1, 1, 1) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL 799-1");
        ++total; if (Math.abs(sol.champagneTower(2, 1, 1) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 799-2");
        ++total; if (Math.abs(sol.champagneTower(100000009, 33, 17) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL 799-3");
        ++total; if (Math.abs(sol.champagneTower(0, 5, 2) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL 799-4");
        ++total; if (Math.abs(sol.champagneTower(25, 6, 1) - 0.1875) < 1e-9) ++pass; else System.out.println("FAIL 799-5 got " + sol.champagneTower(25, 6, 1));

        ++total; if (Math.abs(sol.champagneTower2(1, 1, 1) - 0.0) < 1e-9) ++pass; else System.out.println("FAIL 799-6");
        ++total; if (Math.abs(sol.champagneTower2(2, 1, 1) - 0.5) < 1e-9) ++pass; else System.out.println("FAIL 799-7");
        ++total; if (Math.abs(sol.champagneTower2(100000009, 33, 17) - 1.0) < 1e-9) ++pass; else System.out.println("FAIL 799-8");
        ++total; if (Math.abs(sol.champagneTower2(25, 6, 1) - 0.1875) < 1e-9) ++pass; else System.out.println("FAIL 799-9");

        if (pass == total) {
            System.out.println("== 0799 PASS ==");
        } else {
            System.out.println("== 0799 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(query_row^2)O(query_row)
解法二O(query_row^2)O(query_row^2)

800. Similar RGB Color(相似 RGB 颜色)

题目描述

RGB 颜色 "#AABBCC" 可以简写成 "#ABC"。
例如,"#15c" 其实是 "#1155cc" 的简写。
现在,假如我们分别定义两个颜色 "#ABCDEF" 和 "#UVWXYZ",则他们的相似度可以通过这个
表达式 -(AB - UV)^2 - (CD - WX)^2 - (EF - YZ)^2 来计算。
那么给你一个按 "#ABCDEF" 形式定义的字符串 color 表示 RGB 颜色,请你以字符串形式,
返回一个与它相似度最大且可以简写的颜色(比如,可以表示成类似 "#XYZ" 的形式)。
任何具有相同的(最大)相似度的答案都会被视为正确答案。

示例 1

  • 输入:color = "#09f166"
  • 输出:"#11ee66"
  • 解释:因为相似度计算得出 -(0x09 - 0x11)^2 - (0xf1 - 0xee)^2 - (0x66 - 0x66)^2 = -64 -9 -0 = -73
    这已经是所有可以简写的颜色中最相似的了

示例 2

  • 输入:color = "#4e3fe1"
  • 输出:"#5544dd"
    提示:
  • color.length == 7
  • color[0] == '#'
  • 对于任何 i > 0,color[i] 都是一个在范围 ['0', 'f'] 内的 16 进制数

解题思路

简写颜色每个分量的取值只能是 0x00、0x11、...、0xff(17 的倍数)。三个通道相互独立,
分别找离原分量最近的 17 的倍数即可,无需枚举 16^3 种组合。
解法一(四舍五入取模,主提交):对两位十六进制分量 q,q/17 的余数 > 8 时进位,
再乘以 17 即得最接近的简写分量。
解法二(枚举 16 个候选):对每个通道遍历 0x00..0xff 中 16 个简写候选求最小距离(对照实现,
直观暴力)。

代码实现

class Solution {

    // 解法一:除 17 四舍五入
    public String similarRGB(String color) {
        return "#" + f(color.substring(1, 3)) + f(color.substring(3, 5)) + f(color.substring(5, 7));
    }

    private String f(String seg) {
        int q = Integer.parseInt(seg, 16);
        int x = q / 17;
        if (q % 17 > 8) {
            ++x;
        }
        return String.format("%02x", 17 * x);
    }

    // 解法二:枚举 16 个候选取最近
    public String similarRGB2(String color) {
        StringBuilder sb = new StringBuilder("#");
        for (int i = 1; i <= 5; i += 2) {
            int val = Integer.parseInt(color.substring(i, i + 2), 16);
            int best = 0;
            int bestDiff = Integer.MAX_VALUE;
            for (int x = 0; x < 16; ++x) {
                int cand = 17 * x;
                int diff = Math.abs(cand - val);
                if (diff < bestDiff) {
                    bestDiff = diff;
                    best = cand;
                }
            }
            sb.append(String.format("%02x", best));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;

        ++total; if (sol.similarRGB("#09f166").equals("#11ee66")) ++pass; else System.out.println("FAIL 800-1 " + sol.similarRGB("#09f166"));
        ++total; if (sol.similarRGB("#4e3fe1").equals("#5544dd")) ++pass; else System.out.println("FAIL 800-2 " + sol.similarRGB("#4e3fe1"));
        ++total; if (sol.similarRGB("#000000").equals("#000000")) ++pass; else System.out.println("FAIL 800-3");
        ++total; if (sol.similarRGB("#ffffff").equals("#ffffff")) ++pass; else System.out.println("FAIL 800-4");
        ++total; if (sol.similarRGB("#123456").equals("#113355")) ++pass; else System.out.println("FAIL 800-5 " + sol.similarRGB("#123456"));

        ++total; if (sol.similarRGB2("#09f166").equals("#11ee66")) ++pass; else System.out.println("FAIL 800-6");
        ++total; if (sol.similarRGB2("#4e3fe1").equals("#5544dd")) ++pass; else System.out.println("FAIL 800-7");
        ++total; if (sol.similarRGB2("#123456").equals("#113355")) ++pass; else System.out.println("FAIL 800-8");

        if (pass == total) {
            System.out.println("== 0800 PASS ==");
        } else {
            System.out.println("== 0800 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)O(1)
解法二O(16 * 3)O(1)