阅读视图

发现新文章,点击刷新页面。

四种方法:哈希集合/摩尔投票/邻近元素/随机(Python/Java/C++/Go)

方法一:哈希集合(暴力)

遍历 $\textit{nums}$,同时用一个哈希集合记录遍历过的数。

如果遍历到相同数字(哈希集合中有),由于题目保证只有一个数字是重复的,返回这个数。

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        seen = set()
        for x in nums:
            if x in seen:
                return x
            seen.add(x)
class Solution {
    public int repeatedNTimes(int[] nums) {
        HashSet<Integer> seen = new HashSet<>();
        for (int x : nums) {
            if (!seen.add(x)) { // x 在 seen 中
                return x;
            }
        }
        return -1; // 代码不会执行到这里
    }
}
class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        unordered_set<int> seen;
        for (int x : nums) {
            if (!seen.insert(x).second) { // x 在 seen 中
                return x;
            }
        }
        return -1; // 代码不会执行到这里
    }
};
func repeatedNTimes(nums []int) int {
seen := map[int]struct{}{}
for _, x := range nums {
if _, ok := seen[x]; ok {
return x
}
seen[x] = struct{}{}
}
panic(-1)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

如何做到 $\mathcal{O}(1)$ 空间?下面再介绍三种方法。

方法二:摩尔投票

请先完成 169. 多数元素我的题解

为了让出现 $n$ 次的那个数变成绝对众数,我们可以分类讨论:

  • 如果 $\textit{nums}[0]$ 在下标 $[1,n-1]$ 中出现过,那么返回 $\textit{nums}[0]$。
  • 否则,去掉 $\textit{nums}[0]$,剩下 $2n-1$ 个数,出现次数为 $n$ 的那个数变成绝对众数,可以用 169 题的算法解决。

这两件事情可以在同一个循环中完成。

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        ans = hp = 0
        for x in nums[1:]:  # 也可以写 for i in range(1, len(nums)) 避免切片
            if x == nums[0]:
                return x
            if hp == 0:  # x 是初始擂主,生命值为 1
                ans, hp = x, 1
            else:  # 比武,同门加血,否则扣血
                hp += 1 if x == ans else -1
        return ans
class Solution {
    public int repeatedNTimes(int[] nums) {
        int ans = 0;
        int hp = 0;
        for (int i = 1; i < nums.length; i++) {
            int x = nums[i];
            if (x == nums[0]) {
                return x;
            }
            if (hp == 0) { // x 是初始擂主,生命值为 1
                ans = x;
                hp = 1;
            } else { // 比武,同门加血,否则扣血
                hp += x == ans ? 1 : -1;
            }
        }
        return ans;
    }
}
class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        int ans = 0, hp = 0;
        for (int i = 1; i < nums.size(); i++) {
            int x = nums[i];
            if (x == nums[0]) {
                return x;
            }
            if (hp == 0) { // x 是初始擂主,生命值为 1
                ans = x;
                hp = 1;
            } else { // 比武,同门加血,否则扣血
                hp += x == ans ? 1 : -1;
            }
        }
        return ans;
    }
};
func repeatedNTimes(nums []int) (ans int) {
hp := 0
for _, x := range nums[1:] {
if x == nums[0] {
return x
}
if hp == 0 { // x 是初始擂主,生命值为 1
ans, hp = x, 1
} else if x == ans { // 比武,同门加血,否则扣血
hp++
} else {
hp--
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

方法三:检查邻近元素

设出现次数为 $n$ 的那个数为 $x$。

如果相邻两个 $x$ 之间都至少有一个数,那么 $\textit{nums}$ 至少要有 $2n-1$ 个数,这是合法的。

如果相邻两个 $x$ 之间都至少有两个数,那么 $\textit{nums}$ 至少要有 $3n-2$ 个数。

  • 如果 $n=2$,这是合法的,例如 $[3,1,2,3]$。
  • 如果 $n\ge 3$,那么 $3n-2 > 2n$,不合法。这意味着,当 $n\ge 3$ 时,一定存在 $\textit{nums}[i] = \textit{nums}[i-1]$ 或者 $\textit{nums}[i] = \textit{nums}[i-2]$。

为了兼容 $n=2$ 的情况,我们可以判断 $\textit{nums}[i]$ 是否与下标 $[i-3, i-1]$ 中的元素相等。

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        for i in range(1, len(nums)):
            x = nums[i]
            if x == nums[i - 1] or \
               i > 1 and x == nums[i - 2] or \
               i > 2 and x == nums[i - 3]:
                return x
class Solution {
    public int repeatedNTimes(int[] nums) {
        for (int i = 1; ; i++) {
            int x = nums[i];
            if (x == nums[i - 1] || 
                i > 1 && x == nums[i - 2] || 
                i > 2 && x == nums[i - 3]) {
                return x;
            }
        }
    }
}
class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        for (int i = 1; ; i++) {
            int x = nums[i];
            if (x == nums[i - 1] || 
                i > 1 && x == nums[i - 2] || 
                i > 2 && x == nums[i - 3]) {
                return x;
            }
        }
    }
};
func repeatedNTimes(nums []int) int {
for i := 1; ; i++ {
x := nums[i]
if x == nums[i-1] ||
i > 1 && x == nums[i-2] ||
i > 2 && x == nums[i-3] {
return x
}
}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

方法四:随机

在 $\textit{nums}$ 中随机选择两个下标不同的元素,如果两数相等,即找到了重复元素。

首先,在 $[0,n-1]$ 中随机一个数,作为下标 $i$。

然后,我们要在 $[0,i-1]\cup [i+1,n-1]$ 中随机另一个下标 $j$。

但是,标准库只支持在一个连续范围中随机元素,如何在一个间断的区间中随机元素呢?

考虑映射,把 $[0,n-2]$ 映射到 $[0,i-1]\cup [i+1,n-1]$ 中:

$$
f(x) =
\begin{cases}
x, & 0\le x \le i-1 \
x+1, & i\le x \le n-2 \
\end{cases}
$$

具体地,在 $[0,n-2]$ 中随机一个数 $x$:

  • 如果 $x < i$,那么把 $x$ 作为下标 $j$。
  • 如果 $x \ge i$,那么把 $x+1$ 作为下标 $j$。
class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        n = len(nums)
        while True:
            # 在 [0, n-1] 中随机生成两个不同下标
            i = randrange(n)
            j = randrange(n - 1)
            if j >= i:
                j += 1
            if nums[i] == nums[j]:
                return nums[i]
class Solution {
    private static final Random rand = new Random();

    public int repeatedNTimes(int[] nums) {
        int n = nums.length;
        while (true) {
            // 在 [0, n-1] 中随机生成两个不同下标
            int i = rand.nextInt(n);
            int j = rand.nextInt(n - 1);
            if (j >= i) {
                j++;
            }
            if (nums[i] == nums[j]) {
                return nums[i];
            }
        }
    }
}
class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        int n = nums.size();
        mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
        while (true) {
            // 在 [0, n-1] 中随机生成两个不同下标
            int i = uniform_int_distribution<>(0, n - 1)(rng);
            int j = uniform_int_distribution<>(0, n - 2)(rng);
            if (j >= i) {
                j++;
            }
            if (nums[i] == nums[j]) {
                return nums[i];
            }
        }
    }
};
func repeatedNTimes(nums []int) int {
n := len(nums)
for {
// 在 [0, n-1] 中随机生成两个不同下标
i := rand.Intn(n)
j := rand.Intn(n - 1)
if j >= i {
j++
}
if nums[i] == nums[j] {
return nums[i]
}
}
}

复杂度分析

  • 时间复杂度:期望 $\mathcal{O}(1)$。在 $\textit{nums}$ 中随机选择两个下标不同的元素,两数相等的概率为 $\dfrac{n}{2n}\times \dfrac{n-1}{2n-1}$,当 $n=2$ 时概率为 $p=\dfrac{1}{6}$,当 $n$ 增大时概率 $p\to\dfrac{1}{4}$,期望循环次数 $\dfrac{1}{p} \le 6 = \mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面数学题单的「§6.2 随机化技巧」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-在长度 2N 的数组中找出重复 N 次的元素🟢

给你一个整数数组 nums ,该数组具有以下属性:

  • nums.length == 2 * n.
  • nums 包含 n + 1不同的 元素
  • nums 中恰有一个元素重复 n

找出并返回重复了 n 次的那个元素。

 

示例 1:

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

示例 2:

输入:nums = [2,1,2,5,3,2]
输出:2

示例 3:

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

 

提示:

  • 2 <= n <= 5000
  • nums.length == 2 * n
  • 0 <= nums[i] <= 104
  • numsn + 1 不同的 元素组成,且其中一个元素恰好重复 n

【宫水三叶】简单模拟题

计数模拟

根据题目给定的三个条件可推断出:数组中仅有一个元素出现多次,其余元素均出现一次。

又利用数据范围为 $10^4$,我们可以使用数组充当哈希表进行计数,当出现一个 $nums[i]$ 重复出现即是答案。

代码:

###Java

class Solution {
    int[] cnts = new int[10010];
    public int repeatedNTimes(int[] nums) {
        for (int x : nums) {
            if (++cnts[x] > 1) return x;
        }
        return -1;
    }
}
  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(C)$

构造共性

假设重复出现的数字是 $x$,数字 $x$ 重复了 $n$ 次,要将这 $n$ 个相同的 $x$ 间隔开,需要 $n - 1$ 个额外数字,而实际上我们除 $x$ 以外还有 $n$ 个额外数字(数字总数为 $n + 1$ 个),因此在我们所能构造出的所有排列方式中,最多 使相邻 $x$ 之间间隔了 $2$ 个额外数字。

对于每个 $nums[i]$ 而言,我们检查往前的三个位置(若有),如果有重复相等情况,说明当前的 $nums[i]$ 即是答案。

代码:

###Java

class Solution {
    public int repeatedNTimes(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (i - 1 >= 0 && nums[i - 1] == t) return t;
            if (i - 2 >= 0 && nums[i - 2] == t) return t;
            if (i - 3 >= 0 && nums[i - 3] == t) return t;
        }
        return -1;
    }
}
  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$

最后

如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

也欢迎你 关注我 和 加入我们的「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。

所有题解已经加入 刷题指南,欢迎 star 哦 ~

【负雪明烛】图解算法:最小间隔

大家好,我是 @负雪明烛。点击右上方的「+关注,优质题解不间断!

题目大意

今天的题目是说,有一个长度为 $2*n$ 的数组,其中有 $n + 1$ 种不同数字,其中有一种数字重复了 $n$ 次。

让我们找出这个重复 $n$ 次的数字。

解题方法

官方题解中的「哈希表」、「随机选择」方法,都很好理解。

我来解释一下「数学方法」。

结论就是:重复的数字之间的间隔最大为 $2$。

题目给了 $n >= 2$,也就是说数组的长度最少为 $4$。

我们看一下,当数组长度为 $4$ 时,如果让重复的数字间隔尽可能大,我们该怎么做?

961. 在长度 2N 的数组中找出重复 N 次的元素.001.png

把重复的数字放在数组的两头,这样它们之间的间隔是最大的,也就是 $2$。

当数组长度为 $6$ 时,相当于在长度为 $4$ 的数组后面又追加了两个数字,其中还必须有一个重复数字。

961. 在长度 2N 的数组中找出重复 N 次的元素.002.png

你会发现,无论是将长度为 $4$ 的数组重新排列,还是换追加的两个数字的位置,重复数字的 「最小间隔」 都是 $1$。

因此,当数组长度 $>= 6$ 时,重复数字之间的 「最小间隔」 都是 $1$。

综上,重复数字的「最小间隔」为 $1$ 或者 $2$。即,下标之差的最大值为$2$或者 $3$。

我们从左到右遍历数组,分别比较

  • $nums[i]$ 与 $nums[i + 1]$
  • $nums[i]$ 与 $nums[i + 2]$
  • $nums[i]$ 与 $nums[i + 3]$

一定能找到重复数字。

961. 在长度 2N 的数组中找出重复 N 次的元素.003.png

最坏情况是什么呢?

重复数字都集中在数组的右半部分,遍历到右边这一半的数组才能找到重复数字。

961. 在长度 2N 的数组中找出重复 N 次的元素.004.png

代码

使用两层 $for$ 循环,外层 $for$ 是为了遍历,内层 $for$ 是为了遍历可能的间隔。

###Java

class Solution {
    public int repeatedNTimes(int[] nums) {
        int N = nums.length;
        for (int i = 0; i < nums.length - 1; ++i) {
            for (int j = 1; j <= 3 && i + j < N; ++j) {
                if (nums[i] == nums[i + j])
                    return nums[i];
            }
        }
        return -1;
    }
}

###Python

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        N = len(nums)
        for i in range(N - 1):
            for j in range(1, 4):
                if nums[i] == nums[min(i + j, N - 1)]:
                    return nums[i]

###C++

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        const int N = nums.size();
        for (int i = 0; i < nums.size() - 1; ++i) {
            for (int j = 1; j <= 3 && i + j < N; ++j) {
                if (nums[i] == nums[i + j])
                    return nums[i];
            }
        }
        return -1;
    }
};

总结

  1. 形象地理解一下,是不是更好记?
  2. 注意数组下标不要越界。

我是 @负雪明烛 ,刷算法题 1000 多道,写了 1000 多篇算法题解,收获阅读量 300 万。
关注我,你将不会错过我的精彩动画题解、面试题分享、组队刷题活动,进入主页 @负雪明烛 右侧有刷题组织,从此刷题不再孤单。

在长度 2N 的数组中找出重复 N 次的元素

方法一:哈希表

思路与算法

记重复 $n$ 次的元素为 $x$。由于数组 $\textit{nums}$ 中有 $n+1$ 个不同的元素,而其长度为 $2n$,那么数组中剩余的元素均只出现了一次。也就是说,我们只需要找到重复出现的元素即为答案。

因此我们可以对数组进行一次遍历,并使用哈希集合存储已经出现过的元素。如果遍历到了哈希集合中的元素,那么返回该元素作为答案。

代码

###C++

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        unordered_set<int> found;
        for (int num: nums) {
            if (found.count(num)) {
                return num;
            }
            found.insert(num);
        }
        // 不可能的情况
        return -1;
    }
};

###Java

class Solution {
    public int repeatedNTimes(int[] nums) {
        Set<Integer> found = new HashSet<Integer>();
        for (int num : nums) {
            if (!found.add(num)) {
                return num;
            }
        }
        // 不可能的情况
        return -1;
    }
}

###C#

public class Solution {
    public int RepeatedNTimes(int[] nums) {
        ISet<int> found = new HashSet<int>();
        foreach (int num in nums) {
            if (!found.Add(num)) {
                return num;
            }
        }
        // 不可能的情况
        return -1;
    }
}

###Python

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        found = set()

        for num in nums:
            if num in found:
                return num
            found.add(num)
        
        # 不可能的情况
        return -1

###C

struct HashItem {
    int key;
    UT_hash_handle hh;
};

void freeHash(struct HashItem **obj) {
    struct HashItem *curr, *tmp;
    HASH_ITER(hh, *obj, curr, tmp) {
        HASH_DEL(*obj, curr);  
        free(curr);        
    }
}

int repeatedNTimes(int* nums, int numsSize){
    struct HashItem *found = NULL;
    for (int i = 0; i < numsSize; i++) {
        struct HashItem *pEntry = NULL;
        HASH_FIND_INT(found, &nums[i], pEntry);
        if (pEntry != NULL) {
            freeHash(&found);
            return nums[i];
        } else {
            pEntry = (struct HashItem *)malloc(sizeof(struct HashItem));
            pEntry->key = nums[i];
            HASH_ADD_INT(found, key, pEntry);
        }
    }
    // 不可能的情况
    freeHash(&found);
    return -1;
}

###go

func repeatedNTimes(nums []int) int {
    found := map[int]bool{}
    for _, num := range nums {
        if found[num] {
            return num
        }
        found[num] = true
    }
    return -1 // 不可能的情况
}

###JavaScript

var repeatedNTimes = function(nums) {
    const found = new Set();
    for (const num of nums) {
        if (found.has(num)) {
            return num;
        }
        found.add(num);
    }
    // 不可能的情况
    return -1;
};

复杂度分析

  • 时间复杂度:$O(n)$。我们只需要对数组 $\textit{nums}$ 进行一次遍历。

  • 空间复杂度:$O(n)$,即为哈希集合需要使用的空间。

方法二:数学

思路与算法

我们可以考虑重复的元素 $x$ 在数组 $\textit{nums}$ 中出现的位置。

如果相邻的 $x$ 之间至少都隔了 $2$ 个位置,那么数组的总长度至少为:

$$
n + 2(n - 1) = 3n - 2
$$

当 $n > 2$ 时,$3n-2 > 2n$,不存在满足要求的数组。因此一定存在两个相邻的 $x$,它们的位置是连续的,或者只隔了 $1$ 个位置。

当 $n = 2$ 时,数组的长度最多为 $2n = 4$,因此最多只能隔 $2$ 个位置。

这样一来,我们只需要遍历所有间隔 $2$ 个位置及以内的下标对,判断对应的元素是否相等即可。

代码

###C++

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        int n = nums.size();
        for (int gap = 1; gap <= 3; ++gap) {
            for (int i = 0; i + gap < n; ++i) {
                if (nums[i] == nums[i + gap]) {
                    return nums[i];
                }
            }
        }
        // 不可能的情况
        return -1;
    }
};

###Java

class Solution {
    public int repeatedNTimes(int[] nums) {
        int n = nums.length;
        for (int gap = 1; gap <= 3; ++gap) {
            for (int i = 0; i + gap < n; ++i) {
                if (nums[i] == nums[i + gap]) {
                    return nums[i];
                }
            }
        }
        // 不可能的情况
        return -1;
    }
}

###C#

public class Solution {
    public int RepeatedNTimes(int[] nums) {
        int n = nums.Length;
        for (int gap = 1; gap <= 3; ++gap) {
            for (int i = 0; i + gap < n; ++i) {
                if (nums[i] == nums[i + gap]) {
                    return nums[i];
                }
            }
        }
        // 不可能的情况
        return -1;
    }
}

###Python

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        n = len(nums)
        for gap in range(1, 4):
            for i in range(n - gap):
                if nums[i] == nums[i + gap]:
                    return nums[i]
        
        # 不可能的情况
        return -1

###C

int repeatedNTimes(int* nums, int numsSize) {
    for (int gap = 1; gap <= 3; ++gap) {
        for (int i = 0; i + gap < numsSize; ++i) {
            if (nums[i] == nums[i + gap]) {
                return nums[i];
            }
        }
    }
    // 不可能的情况
    return -1;
}

###go

func repeatedNTimes(nums []int) int {
    for gap := 1; gap <= 3; gap++ {
        for i, num := range nums[:len(nums)-gap] {
            if num == nums[i+gap] {
                return num
            }
        }
    }
    return -1 // 不可能的情况
}

###JavaScript

var repeatedNTimes = function(nums) {
    const n = nums.length;
    for (let gap = 1; gap <= 3; ++gap) {
        for (let i = 0; i + gap < n; ++i) {
            if (nums[i] === nums[i + gap]) {
                return nums[i];
            }
        }
    }
    // 不可能的情况
    return -1;
};

复杂度分析

  • 时间复杂度:$O(n)$。我们最多对数组进行三次遍历(除了 $n=2$ 之外,最多两次遍历)。

  • 空间复杂度:$O(1)$。

方法三:随机选择

思路与算法

我们可以每次随机选择两个不同的下标,判断它们对应的元素是否相等即可。如果相等,那么返回任意一个作为答案。

代码

###C++

class Solution {
public:
    int repeatedNTimes(vector<int>& nums) {
        int n = nums.size();
        mt19937 gen{random_device{}()};
        uniform_int_distribution<int> dis(0, n - 1);

        while (true) {
            int x = dis(gen), y = dis(gen);
            if (x != y && nums[x] == nums[y]) {
                return nums[x];
            }
        }
    }
};

###Java

class Solution {
    public int repeatedNTimes(int[] nums) {
        int n = nums.length;
        Random random = new Random();

        while (true) {
            int x = random.nextInt(n), y = random.nextInt(n);
            if (x != y && nums[x] == nums[y]) {
                return nums[x];
            }
        }
    }
}

###C#

public class Solution {
    public int RepeatedNTimes(int[] nums) {
        int n = nums.Length;
        Random random = new Random();

        while (true) {
            int x = random.Next(n), y = random.Next(n);
            if (x != y && nums[x] == nums[y]) {
                return nums[x];
            }
        }
    }
}

###Python

class Solution:
    def repeatedNTimes(self, nums: List[int]) -> int:
        n = len(nums)

        while True:
            x, y = random.randrange(n), random.randrange(n)
            if x != y and nums[x] == nums[y]:
                return nums[x]

###C

int repeatedNTimes(int* nums, int numsSize) {
    srand(time(NULL));
    while (true) {
        int x = random() % numsSize, y = random() % numsSize;
        if (x != y && nums[x] == nums[y]) {
            return nums[x];
        }
    }
}

###go

func repeatedNTimes(nums []int) int {
    n := len(nums)
    for {
        x, y := rand.Intn(n), rand.Intn(n)
        if x != y && nums[x] == nums[y] {
            return nums[x]
        }
    }
}

###JavaScript

var repeatedNTimes = function(nums) {
    const n = nums.length;

    while (true) {
        const x = Math.floor(Math.random() * n), y = Math.floor(Math.random() * n);
        if (x !== y && nums[x] === nums[y]) {
            return nums[x];
        }
    }
};

复杂度分析

  • 时间复杂度:期望 $O(1)$。选择两个相同元素的概率为 $\dfrac{n}{2n} \times \dfrac{n-1}{2n} \approx \dfrac{1}{4}$,因此期望 $4$ 次结束循环。

  • 空间复杂度:$O(1)$。

每日一题-加一🟢

给定一个表示 大整数 的整数数组 digits,其中 digits[i] 是整数的第 i 位数字。这些数字按从左到右,从最高位到最低位排列。这个大整数不包含任何前导 0

将大整数加 1,并返回结果的数字数组。

 

示例 1:

输入:digits = [1,2,3]
输出:[1,2,4]
解释:输入数组表示数字 123。
加 1 后得到 123 + 1 = 124。
因此,结果应该是 [1,2,4]。

示例 2:

输入:digits = [4,3,2,1]
输出:[4,3,2,2]
解释:输入数组表示数字 4321。
加 1 后得到 4321 + 1 = 4322。
因此,结果应该是 [4,3,2,2]。

示例 3:

输入:digits = [9]
输出:[1,0]
解释:输入数组表示数字 9。
加 1 得到了 9 + 1 = 10。
因此,结果应该是 [1,0]。

 

提示:

  • 1 <= digits.length <= 100
  • 0 <= digits[i] <= 9
  • digits 不包含任何前导 0

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

题目让我们模拟加一运算。

比如 $23999+1 = 24000$,$999+1=1000$。

算法:

  1. 从右往左找第一个不等于 $9$ 的数,记作 $\textit{digits}[i]$。
  2. 进位,把 $\textit{digits}[i]$ 加一,把下标在 $[i+1,n-1]$ 中的数全变成 $0$。
  3. 特别地,如果所有数都等于 $9$,那么答案为 $[1,0,0,\dots,0]$,其中有 $n$ 个 $0$。
class Solution:
    def plusOne(self, digits: List[int]) -> List[int]:
        for i in range(len(digits) - 1, -1, -1):
            if digits[i] < 9:
                digits[i] += 1  # 进位
                return digits
            digits[i] = 0  # 进位数字的右边数字都变成 0

        # digits 全是 9,加一后变成 100...00
        return [1] + digits
class Solution {
    public int[] plusOne(int[] digits) {
        int n = digits.length;
        for (int i = n - 1; i >= 0; i--) {
            if (digits[i] < 9) {
                digits[i]++; // 进位
                return digits;
            }
            digits[i] = 0; // 进位数字的右边数字都变成 0
        }

        // digits 全是 9,加一后变成 100...00
        int[] ans = new int[n + 1];
        ans[0] = 1;
        return ans;
    }
}
class Solution {
public:
    vector<int> plusOne(vector<int>& digits) {
        for (int i = digits.size() - 1; i >= 0; i--) {
            if (digits[i] < 9) {
                digits[i]++; // 进位
                return digits;
            }
            digits[i] = 0; // 进位数字的右边数字都变成 0
        }

        // digits 全是 9,加一后变成 100...00
        digits.push_back(0);
        digits[0] = 1;
        return digits;
    }
};
int* plusOne(int* digits, int digitsSize, int* returnSize) {
    for (int i = digitsSize - 1; i >= 0; i--) {
        if (digits[i] < 9) {
            digits[i]++; // 进位
            *returnSize = digitsSize;
            return digits;
        }
        digits[i] = 0; // 进位数字的右边数字都变成 0
    }

    // digits 全是 9,加一后变成 100...00
    *returnSize = digitsSize + 1;
    int* ans = calloc(digitsSize + 1, sizeof(int));
    ans[0] = 1;
    return ans;
}
func plusOne(digits []int) []int {
    for i, d := range slices.Backward(digits) {
        if d < 9 {
            digits[i]++ // 进位
            return digits
        }
        digits[i] = 0 // 进位数字的右边数字都变成 0
    }

    // digits 全是 9,加一后变成 100...00
    digits = append(digits, 0)
    digits[0] = 1
    return digits
}
var plusOne = function(digits) {
    for (let i = digits.length - 1; i >= 0; i--) {
        if (digits[i] < 9) {
            digits[i]++; // 进位
            return digits;
        }
        digits[i] = 0; // 进位数字的右边数字都变成 0
    }

    // digits 全是 9,加一后变成 100...00
    digits.push(0);
    digits[0] = 1;
    return digits;
};
impl Solution {
    pub fn plus_one(mut digits: Vec<i32>) -> Vec<i32> {
        for d in digits.iter_mut().rev() {
            if *d < 9 {
                *d += 1; // 进位
                return digits;
            }
            *d = 0; // 进位数字的右边数字都变成 0
        }

        // digits 全是 9,加一后变成 100...00
        digits.push(0);
        digits[0] = 1;
        digits
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(m)$,其中 $m$ 是 $\textit{digits}$ 末尾 $9$ 的个数。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

画解算法:66. 加一

解题思路

  • 标签:数组遍历
  • 这道题需要整理出来有哪几种情况,在进行处理会更舒服
  1. 末位无进位,则末位加一即可,因为末位无进位,前面也不可能产生进位,比如 45 => 46
  2. 末位有进位,在中间位置进位停止,则需要找到进位的典型标志,即为当前位 $%10$ 后为 0,则前一位加 1,直到不为 0 为止,比如 499 => 500
  3. 末位有进位,并且一直进位到最前方导致结果多出一位,对于这种情况,需要在第 2 种情况遍历结束的基础上,进行单独处理,比如 999 => 1000
  • 在下方的 Java 和 JavaScript 代码中,对于第三种情况,对其他位进行了赋值 0 处理,Java 比较 tricky 直接 new 数组即可,JavaScript 则使用了 ES6 语法进行赋值
  • 时间复杂度:$O(n)$

代码

###Java

class Solution {
    public int[] plusOne(int[] digits) {
        int len = digits.length;
        for(int i = len - 1; i >= 0; i--) {
            digits[i]++;
            digits[i] %= 10;
            if(digits[i]!=0)
                return digits;
        }
        digits = new int[len + 1];
        digits[0] = 1;
        return digits;
    }
}

###JavaScript

/**
 * @param {number[]} digits
 * @return {number[]}
 */
var plusOne = function(digits) {
    const len = digits.length;
    for(let i = len - 1; i >= 0; i--) {
        digits[i]++;
        digits[i] %= 10;
        if(digits[i]!=0)
            return digits;
    }
    digits = [...Array(len + 1)].map(_=>0);;
    digits[0] = 1;
    return digits;
};

画解

<frame_00001.png,frame_00002.png,frame_00003.png,frame_00004.png>

想看大鹏画解更多高频面试题,欢迎阅读大鹏的 LeetBook:《画解剑指 Offer 》,O(∩_∩)O

Java 数学解题

解题思路:

根据题意加一,没错就是加一这很重要,因为它是只加一的所以有可能的情况就只有两种:

  1. 除 $9$ 之外的数字加一;
  2. 数字 $9$。

加一得十进一位个位数为 $0$ 加法运算如不出现进位就运算结束了且进位只会是一。

所以只需要判断有没有进位并模拟出它的进位方式,如十位数加 $1$ 个位数置为 $0$,如此循环直到判断没有再进位就退出循环返回结果。

然后还有一些特殊情况就是当出现 $99$、$999$ 之类的数字时,循环到最后也需要进位,出现这种情况时需要手动将它进一位。

###Java

class Solution {
    public int[] plusOne(int[] digits) {
        for (int i = digits.length - 1; i >= 0; i--) {
            digits[i]++;
            digits[i] = digits[i] % 10;
            if (digits[i] != 0) return digits;
        }
        digits = new int[digits.length + 1];
        digits[0] = 1;
        return digits;
    }
}

PS:本人并非大佬,这是第一次写思路解释,如有写的不好的地方请多多包涵,哈哈哈

每日一题-矩阵中的幻方🟡

3 x 3 的幻方是一个填充有 19  的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。

给定一个由整数组成的row x col 的 grid,其中有多少个 3 × 3 的 “幻方” 子矩阵?

注意:虽然幻方只能包含 1 到 9 的数字,但 grid 可以包含最多15的数字。

 

示例 1:

输入: grid = [[4,3,8,4],[9,5,1,9],[2,7,6,2]
输出: 1
解释: 
下面的子矩阵是一个 3 x 3 的幻方:

而这一个不是:

总的来说,在本示例所给定的矩阵中只有一个 3 x 3 的幻方子矩阵。

示例 2:

输入: grid = [[8]]
输出: 0

 

提示:

  • row == grid.length
  • col == grid[i].length
  • 1 <= row, col <= 10
  • 0 <= grid[i][j] <= 15

无需计算第三行列的和,以及对角线的和,数学证明(Python/Java/C++/Go)

幻方正中心一定是 5

$3\times 3$ 的幻方必须包含 $1$ 到 $9$ 所有数字,这意味着幻方元素和为 $1+2+\cdots + 9 = 45$。

由于每行的和都要相等,所以每行的和为 $\dfrac{45}{3} = 15$。这也是每列的和,对角线的和。

有 $4$ 条经过幻方正中心的线:第二行,第二列,两条对角线。

这 $4$ 条线:

  • 覆盖了 $3\times 3$ 幻方的每个数,正中心的数覆盖了 $4$ 次(多覆盖了 $3$ 次),其余数恰好覆盖一次。
  • 总和为 $15\cdot 4 = 60$。

设正中心的数为 $x$ ,则有

$$
1+2+\cdots + 9 + 3x = 60
$$

解得

$$
x = 5
$$

换言之(逆否命题),如果 $3\times 3$ 矩阵正中心的数不是 $5$,那么该矩阵必然不是幻方。

无需计算第三行的和,第三列的和

如果 $3\times 3$ 矩阵包含 $1$ 到 $9$ 所有整数,且前两行的和都是 $15$,那么第三行的和为

$$
1+2+\cdots+9-15-15 = 15
$$

同理,如果前两列的和都是 $15$,那么第三列的和必然是 $15$。

无需计算对角线的和

如果 $3\times 3$ 矩阵:

  • 正中心的数是 $5$。
  • 包含 $1$ 到 $9$ 所有整数。
  • 前两行的和都是 $15$。
  • 前两列的和都是 $15$。

下面证明:矩阵对角线的和一定都是 $15$。

设矩阵为

$$
\begin{bmatrix}
a & b & c \
d & 5 & f \
g & h & i \
\end{bmatrix}
$$

由于正中心的数是 $5$,只需证明对角两数之和等于 $10$,即 $a+i=c+g=10$。

在 $1$ 到 $9$ 中选两个不同整数相加等于 $10$,只有如下 $4$ 种情况:

  • $1+9=10$。
  • $2+8=10$。
  • $3+7=10$。
  • $4+6=10$。

由于已知 $b+h=d+f=10$,消耗了两种情况,剩余的四个数必然在 $a,c,g,i$ 中。比如 $b+h=1+9$,$d+f=3+7$,那么剩余的 $2,4,6,8$ 必然在 $a,c,g,i$ 中。

比如左上角的 $a=4$,那么 $6$ 在哪?

  • 在右上角?此时 $a+c=10$,由于已知 $a+b+c=15$,得到 $b=5$,与正中心的数相同,矛盾。
  • 在左下角?此时 $a+g=10$,由于已知 $a+d+g=15$,得到 $d=5$,与正中心的数相同,矛盾。
  • 所以 $6$ 只能在右下角,即 $a+i=10$ 成立。
  • 剩余两个数 $2$ 和 $8$ 在另一对角,所以 $c+g=10$ 成立。

用同样的方法可以证明,$a$ 等于其他数的时候,对角的数 $i$ 一定等于 $10-a$。剩余的另一对和为 $10$ 的数在另一对角。

综上,由已知条件可得,两条对角线的和都是 $15$。

如何快速判断矩阵包含 $1$ 到 $9$ 所有数?可以把数字压缩到一个二进制数 $\textit{mask}$ 中,$\textit{mask}$ 从低到高的 $i$ 位是 $1$ 表示 $i$ 在矩阵中。矩阵包含 $1$ 到 $9$ 所有数相当于 $\textit{mask} = 1111111110_{(2)} = 2^{10}-2=1022$。

###py

class Solution:
    def numMagicSquaresInside(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        ans = 0

        for i in range(m - 2):
            for j in range(n - 2):
                if grid[i + 1][j + 1] != 5:
                    continue

                mask = 0
                r_sum = [0] * 3
                c_sum = [0] * 3
                for r in range(3):
                    for c in range(3):
                        x = grid[i + r][j + c]
                        mask |= 1 << x
                        r_sum[r] += x
                        c_sum[c] += x

                if mask == 1022 and r_sum[0] == r_sum[1] == c_sum[0] == c_sum[1] == 15:
                    ans += 1

        return ans

###java

class Solution {
    public int numMagicSquaresInside(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int ans = 0;

        for (int i = 0; i < m - 2; i++) {
            for (int j = 0; j < n - 2; j++) {
                if (grid[i + 1][j + 1] != 5) {
                    continue;
                }

                int mask = 0;
                int[] rSum = new int[3];
                int[] cSum = new int[3];
                for (int r = 0; r < 3; r++) {
                    for (int c = 0; c < 3; c++) {
                        int x = grid[i + r][j + c];
                        mask |= 1 << x;
                        rSum[r] += x;
                        cSum[c] += x;
                    }
                }

                if (mask == (1 << 10) - 2 &&
                    rSum[0] == 15 && rSum[1] == 15 &&
                    cSum[0] == 15 && cSum[1] == 15) {
                    ans++;
                }
            }
        }

        return ans;
    }
}

###cpp

class Solution {
public:
    int numMagicSquaresInside(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int ans = 0;

        for (int i = 0; i < m - 2; i++) {
            for (int j = 0; j < n - 2; j++) {
                if (grid[i + 1][j + 1] != 5) {
                    continue;
                }

                int mask = 0;
                int r_sum[3]{};
                int c_sum[3]{};
                for (int r = 0; r < 3; r++) {
                    for (int c = 0; c < 3; c++) {
                        int x = grid[i + r][j + c];
                        mask |= 1 << x;
                        r_sum[r] += x;
                        c_sum[c] += x;
                    }
                }

                if (mask == (1 << 10) - 2 &&
                    r_sum[0] == 15 && r_sum[1] == 15 &&
                    c_sum[0] == 15 && c_sum[1] == 15) {
                    ans++;
                }
            }
        }

        return ans;
    }
};

###c

int numMagicSquaresInside(int** grid, int gridSize, int* gridColSize) {
    int m = gridSize, n = gridColSize[0];
    int ans = 0;

    for (int i = 0; i < m - 2; i++) {
        for (int j = 0; j < n - 2; j++) {
            if (grid[i + 1][j + 1] != 5) {
                continue;
            }

            int mask = 0;
            int r_sum[3] = {};
            int c_sum[3] = {};
            for (int r = 0; r < 3; r++) {
                for (int c = 0; c < 3; c++) {
                    int x = grid[i + r][j + c];
                    mask |= 1 << x;
                    r_sum[r] += x;
                    c_sum[c] += x;
                }
            }

            if (mask == (1 << 10) - 2 &&
                r_sum[0] == 15 && r_sum[1] == 15 &&
                c_sum[0] == 15 && c_sum[1] == 15) {
                ans++;
            }
        }
    }

    return ans;
}

###go

func numMagicSquaresInside(grid [][]int) (ans int) {
m, n := len(grid), len(grid[0])
for i := range m - 2 {
for j := range n - 2 {
if grid[i+1][j+1] != 5 {
continue
}

mask := 0
var rSum, cSum [3]int
for r, row := range grid[i : i+3] {
for c, x := range row[j : j+3] {
mask |= 1 << x
rSum[r] += x
cSum[c] += x
}
}

if mask == 1<<10-2 &&
rSum[0] == 15 && rSum[1] == 15 &&
cSum[0] == 15 && cSum[1] == 15 {
ans++
}
}
}
return
}

###js

var numMagicSquaresInside = function (grid) {
    const m = grid.length;
    const n = grid[0].length;
    let ans = 0;

    for (let i = 0; i < m - 2; i++) {
        for (let j = 0; j < n - 2; j++) {
            if (grid[i + 1][j + 1] !== 5) {
                continue;
            }

            let mask = 0;
            const rSum = [0, 0, 0];
            const cSum = [0, 0, 0];
            for (let r = 0; r < 3; r++) {
                for (let c = 0; c < 3; c++) {
                    const x = grid[i + r][j + c];
                    mask |= 1 << x;
                    rSum[r] += x;
                    cSum[c] += x;
                }
            }

            if (mask === (1 << 10) - 2 &&
                rSum[0] === 15 && rSum[1] === 15 &&
                cSum[0] === 15 && cSum[1] === 15) {
                ans++;
            }
        }
    }

    return ans;
};

###rust

impl Solution {
    pub fn num_magic_squares_inside(grid: Vec<Vec<i32>>) -> i32 {
        let m = grid.len();
        let n = grid[0].len();
        let mut ans = 0;

        for i in 0..m.saturating_sub(2) {
            for j in 0..n.saturating_sub(2) {
                if grid[i + 1][j + 1] != 5 {
                    continue;
                }

                let mut mask = 0;
                let mut r_sum = [0; 3];
                let mut c_sum = [0; 3];
                for (r, row) in grid[i..i + 3].iter().enumerate() {
                    for (c, &x) in row[j..j + 3].iter().enumerate() {
                        mask |= 1 << x;
                        r_sum[r] += x;
                        c_sum[c] += x;
                    }
                }

                if mask == (1 << 10) - 2 &&
                    r_sum[0] == 15 && r_sum[1] == 15 &&
                    c_sum[0] == 15 && c_sum[1] == 15 {
                    ans += 1;
                }
            }
        }

        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

非暴力,努力写出优雅的代码,双百

解题思路

首先谈谈三阶幻方,固然是可以通过定义去判断,但写起来很麻烦,效率也不高。实际上,三阶幻方的解是固定的,有以下八种。
image.png
自然是不可能逐个判断是不是这八个之一,大家仔细观察,相信很容易找出这八个解的共同点,首先中间的元素都是五,角上元素都是偶数,中点都是奇数。同一行的解可以通过旋转得到,第一行镜像,可以得到第二行。也就是说,三阶幻方本质只有一种解,其余都是旋转镜像的体现
我们可以依据这一点,写出优雅简洁的代码。

  1. 遍历中间的部分,只有是五的时候,才判断以他为中心的三阶方阵是否是幻方。
  2. 五已经比对过了,只需要比对其他八个元素,由于解的旋转不变特性,这里将八个元素按顺序存放,方便后面比较,顺序如下图。
  3. 解的编码,如何表示旋转镜像,参考旋转数组这题的思想,将前面部分的放在后面就达到了旋转的效果。镜像只需要反向迭代就好了。
    image.png
    4.第二步输入的数组首位元素和8 6 2 4逐个比较决定可能的解,从编码的数组中取出正向镜像两个解比较是否其一,就可确定是否是幻方。

代码

###cpp

class Solution {
public:
    vector<int> m={8,1,6,7,2,9,4,3,8,1,6,7,2,9,4,3};
    int numMagicSquaresInside(vector<vector<int>>& grid) {
        int di[8]={-1,-1,-1,0,1,1,1,0};
        int dj[8]={-1,0,1,1,1,0,-1,-1};
        int count=0;
        for(int i=1;i<grid.size()-1;i++)
            for(int j=1;j<grid[0].size()-1;j++)
                if(grid[i][j]==5){
                    vector<int> around;
                    for(int k=0;k<8;k++)
                        around.push_back(grid[i+di[k]][j+dj[k]]);
                    count+=IsMagic(around);
                }
        return count;
    }
    bool IsMagic(vector<int>& v){
        for(int i=0;i<8;i+=2)
            if(m[i]==v[0])
            return v==vector<int>(m.begin()+i,m.begin()+i+8)
            ||v==vector<int>(m.rbegin()+7-i,m.rbegin()+15-i);
        return false;//奇数元素
    }
};

使用抽屉算法解题?

解题思路

满足幻方的条件必定是中间的数字为5,周围的数字为满足 1->6->7->2->9->4->3->8->1
的这种圆环结构,5的上下左右必定是 1、9、3、7(这四个数字都只能组成两组 15)
并且5的周围的数字其实都可以跳过,这里没作实现
1.利用数组的索引语义记录索引对应的下一个数字
2.若5的下一个值满足条件,且该值的下一行对应列满足条件,顺时针
若5的下一个值满足条件,且该值的上一行对应列满足条件,逆时针
3.总共判断6次即可(前两次已经判断过了)

代码

###java

class Solution {
    public int numMagicSquaresInside(int[][] grid) {
        //if(grid == null || grid.length < 3 || grid[1].length < 3)return 0;
        //由于是1~9的不同数字,那么必定是5在中间,并且其他数字满足顺时针或逆时针
        int[] check = {0,6,9,8,3,0,7,2,1,4};
        int result = 0;
        for(int i=1;i<grid.length-1;i++){
            for(int j=1;j<grid[0].length-1;j++){
                if(grid[i][j] == 5){
                    int next = grid[i][j+1];
                    switch(next){
                        case 1:
                        case 3:
                        case 7:
                        case 9:
                            if(check[next] == grid[i+1][j+1]){
                                if(each(grid,check,i+1,j+1,1))
                                    result++;
                            }else if(check[next] == grid[i-1][j+1]){
                                if(each(grid,check,i-1,j+1,-1))
                                    result++;
                            }
                    }
                    //5的后一位必定不用算    
                    j++;
                }
            }
        }
        return result;
    }

    public boolean each(int[][] grid,int[] check,int x,int y,int ward){
        int cur = grid[x][y];
        int next = check[cur];
        for(int i=y-2;y!=i;){
            if(grid[x][--y] != next)
                return false;
            cur = next;
            next = check[cur];
        }
        for(int i=x-2*ward;x!=i;){
            if(grid[x-=ward][y] != next)
                return false;
            cur = next;
            next = check[cur];
        }
        for(int i=y+2;y!=i;){
            if(grid[x][++y] != next)
                return false;
            cur = next;
            next = check[cur];
        }
        return true;       
    }
}

每日一题-金字塔转换矩阵🟡

你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行 少一个块 ,并且居中。

为了使金字塔美观,只有特定的 三角形图案 是允许的。一个三角形的图案由 两个块 和叠在上面的 单个块 组成。模式是以三个字母字符串的列表形式 allowed 给出的,其中模式的前两个字符分别表示左右底部块,第三个字符表示顶部块。

  • 例如,"ABC" 表示一个三角形图案,其中一个 “C” 块堆叠在一个 'A' 块(左)和一个 'B' 块(右)之上。请注意,这与 "BAC" 不同,"B" 在左下角,"A" 在右下角。

你从作为单个字符串给出的底部的一排积木 bottom 开始,必须 将其作为金字塔的底部。

在给定 bottom 和 allowed 的情况下,如果你能一直构建到金字塔顶部,使金字塔中的 每个三角形图案 都是在 allowed 中的,则返回 true ,否则返回 false

 

示例 1:

输入:bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"]
输出:true
解释:允许的三角形图案显示在右边。
从最底层(第 3 层)开始,我们可以在第 2 层构建“CE”,然后在第 1 层构建“E”。
金字塔中有三种三角形图案,分别是 “BCC”、“CDE” 和 “CEA”。都是允许的。

示例 2:

输入:bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"]
输出:false
解释:允许的三角形图案显示在右边。
从最底层(即第 4 层)开始,创造第 3 层有多种方法,但如果尝试所有可能性,你便会在创造第 1 层前陷入困境。

 

提示:

  • 2 <= bottom.length <= 6
  • 0 <= allowed.length <= 216
  • allowed[i].length == 3
  • 所有输入字符串中的字母来自集合 {'A', 'B', 'C', 'D', 'E', 'F'}
  •  allowed 中所有值都是 唯一的

【图解】回溯 + vis 优化 + 位运算优化(Python/Java/C++/Go)

写一个类似 17. 电话号码的字母组合 的回溯(搜索)。从下往上,从左到右,枚举金字塔的每个格子填什么字母。

lc756-1.png{:width=600px}

比如 $\textit{allowed}$ 中有 $\texttt{AAB}$ 和 $\texttt{AAC}$,那么当 $(i+1,j)$ 和 $(i+1,j+1)$ 都填 $\texttt{A}$ 的时候,$(i,j)$ 可以填 $\texttt{B}$,也可以填 $\texttt{C}$。枚举填哪个

为了快速知道 $\texttt{AA}\to [\texttt{B}, \texttt{C}]$ 的对应关系,可以把 $\textit{allowed}$ 用哈希表(或者二维数组)分组,把 $\textit{allowed}[i]$ 前两个字母对应的第三个字母,记录在一个列表中。

优化前

###py

class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        groups = defaultdict(list)  # 三角形底部两个字母 -> [三角形顶部字母]
        for s in allowed:
            groups[s[:2]].append(s[2])

        n = len(bottom)
        pyramid = [[''] * (i + 1) for i in range(n)]
        pyramid[-1] = bottom

        # 现在准备填 (i, j) 这个格子
        # 返回继续填能否填完所有格子(从下往上填,每行从左到右填)
        def dfs(i: int, j: int) -> bool:
            if i < 0:  # 所有格子都已填完
                return True

            if j == i + 1:  # i 行已填完
                return dfs(i - 1, 0)  # 开始填 i-1 行

            # 枚举 (i, j) 填什么字母
            # 这取决于 (i+1, j) 和 (i+1, j+1) 填的字母
            for top in groups[pyramid[i + 1][j] + pyramid[i + 1][j + 1]]:
                pyramid[i][j] = top
                if dfs(i, j + 1):
                    return True
            return False

        # 从倒数第二行开始填
        return dfs(n - 2, 0)

###java

class Solution {
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        // 三角形底部两个字母 -> [三角形顶部字母]
        List<Character>[][] groups = new ArrayList[6][6];
        for (List<Character>[] row : groups) {
            Arrays.setAll(row, _ -> new ArrayList<>());
        }
        for (String S : allowed) {
            char[] s = S.toCharArray();
            groups[s[0] - 'A'][s[1] - 'A'].add(s[2]);
        }

        int n = bottom.length();
        char[][] pyramid = new char[n][];
        for (int i = 0; i < n - 1; i++) {
            pyramid[i] = new char[i + 1];
        }
        pyramid[n - 1] = bottom.toCharArray();

        // 从倒数第二行开始填
        return dfs(n - 2, 0, pyramid, groups);
    }

    // 现在准备填 (i, j) 这个格子
    // 返回继续填能否填完所有格子(从下往上填,每行从左到右填)
    private boolean dfs(int i, int j, char[][] pyramid, List<Character>[][] groups) {
        if (i < 0) { // 所有格子都已填完
            return true;
        }

        if (j == i + 1) { // i 行已填完
            return dfs(i - 1, 0, pyramid, groups); // 开始填 i-1 行
        }

        // 枚举 (i, j) 填什么字母
        // 这取决于 (i+1, j) 和 (i+1, j+1) 填的字母
        for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
            pyramid[i][j] = top;
            if (dfs(i, j + 1, pyramid, groups)) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    bool pyramidTransition(string bottom, vector<string>& allowed) {
        string groups[6][6]{}; // 三角形底部两个字母 -> [三角形顶部字母]
        for (auto& s : allowed) {
            groups[s[0] - 'A'][s[1] - 'A'] += s[2];
        }

        int n = bottom.size();
        vector<string> pyramid(n);
        for (int i = 0; i < n - 1; i++) {
            pyramid[i].resize(i + 1);
        }
        pyramid[n - 1] = move(bottom);

        // 现在准备填 (i, j) 这个格子
        // 返回继续填能否填完所有格子(从下往上填,每行从左到右填)
        auto dfs = [&](this auto&& dfs, int i, int j) -> bool {
            if (i < 0) { // 所有格子都已填完
                return true;
            }

            if (j == i + 1) { // i 行已填完
                return dfs(i - 1, 0); // 开始填 i-1 行
            }

            // 枚举 (i, j) 填什么字母
            // 这取决于 (i+1, j) 和 (i+1, j+1) 填的字母
            for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
                pyramid[i][j] = top;
                if (dfs(i, j + 1)) {
                    return true;
                }
            }
            return false;
        };

        // 从倒数第二行开始填
        return dfs(n - 2, 0);
    }
};

###go

func pyramidTransition(bottom string, allowed []string) bool {
groups := [6][6][]byte{} // 三角形底部两个字母 -> [三角形顶部字母]
for _, s := range allowed {
a, b := s[0]-'A', s[1]-'A'
groups[a][b] = append(groups[a][b], s[2])
}

n := len(bottom)
pyramid := make([][]byte, n)
for i := range n - 1 {
pyramid[i] = make([]byte, i+1)
}
pyramid[n-1] = []byte(bottom)

// 现在准备填 (i, j) 这个格子
// 返回继续填能否填完所有格子(从下往上填,每行从左到右填)
var dfs func(int, int) bool
dfs = func(i, j int) bool {
if i < 0 { // 所有格子都已填完
return true
}

if j == i+1 { // i 行已填完
return dfs(i-1, 0) // 开始填 i-1 行
}

// 枚举 (i, j) 填什么字母
// 这取决于 (i+1, j) 和 (i+1, j+1) 填的字母
for _, top := range groups[pyramid[i+1][j]-'A'][pyramid[i+1][j+1]-'A'] {
pyramid[i][j] = top
if dfs(i, j+1) {
return true
}
}
return false
}

// 从倒数第二行开始填
return dfs(n-2, 0)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(|\Sigma|^{n^2})$,其中 $n$ 是 $\textit{bottom}$ 的长度,$|\Sigma|=6$ 是字符集合的大小。搜索树的高度为 $\mathcal{O}(n^2)$,每个节点至多有 $|\Sigma|$ 个儿子,所以搜索树有 $\mathcal{O}(|\Sigma|^{n^2})$ 个节点,遍历这棵搜索树需要 $\mathcal{O}(|\Sigma|^{n^2})$ 的时间。
  • 空间复杂度:$\mathcal{O}(m + n^2)$,其中 $m$ 是 $\textit{allowed}$ 的长度。

优化一:减少重复搜索

lc756-2c.png{:width=600px}

###py

class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        groups = defaultdict(list)
        for s in allowed:
            groups[s[:2]].append(s[2])

        n = len(bottom)
        pyramid = [[''] * (i + 1) for i in range(n)]
        pyramid[-1] = bottom

        vis = set()  # 访问标记

        def dfs(i: int, j: int) -> bool:
            if i < 0:
                return True

            if j == i + 1:
                row = ''.join(pyramid[i])
                if row in vis:  # 这一行之前填过一模一样的,继续填,没能填到塔顶
                    return False  # 直接返回
                vis.add(row)
                return dfs(i - 1, 0)

            for top in groups[pyramid[i + 1][j] + pyramid[i + 1][j + 1]]:
                pyramid[i][j] = top
                if dfs(i, j + 1):
                    return True
            return False

        return dfs(n - 2, 0)

###java

class Solution {
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        List<Character>[][] groups = new ArrayList[6][6];
        for (List<Character>[] row : groups) {
            Arrays.setAll(row, _ -> new ArrayList<>());
        }
        for (String S : allowed) {
            char[] s = S.toCharArray();
            groups[s[0] - 'A'][s[1] - 'A'].add(s[2]);
        }

        int n = bottom.length();
        char[][] pyramid = new char[n][];
        for (int i = 0; i < n - 1; i++) {
            pyramid[i] = new char[i + 1];
        }
        pyramid[n - 1] = bottom.toCharArray();

        Set<String> vis = new HashSet<>(); // 访问标记

        return dfs(n - 2, 0, pyramid, vis, groups);
    }

    private boolean dfs(int i, int j, char[][] pyramid, Set<String> vis, List<Character>[][] groups) {
        if (i < 0) {
            return true;
        }

        if (j == i + 1) {
            String row = new String(pyramid[i]);
            if (!vis.add(row)) { // 这一行之前填过一模一样的,继续填,没能填到塔顶
                return false; // 直接返回
            }
            return dfs(i - 1, 0, pyramid, vis, groups);
        }

        for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
            pyramid[i][j] = top;
            if (dfs(i, j + 1, pyramid, vis, groups)) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    bool pyramidTransition(string bottom, vector<string>& allowed) {
        string groups[6][6];
        for (auto& s : allowed) {
            groups[s[0] - 'A'][s[1] - 'A'] += s[2];
        }

        int n = bottom.size();
        vector<string> pyramid(n);
        for (int i = 0; i < n - 1; i++) {
            pyramid[i].resize(i + 1);
        }
        pyramid[n - 1] = move(bottom);

        unordered_set<string> vis; // 访问标记

        auto dfs = [&](this auto&& dfs, int i, int j) -> bool {
            if (i < 0) {
                return true;
            }

            if (j == i + 1) {
                if (!vis.insert(pyramid[i]).second) { // 这一行之前填过一模一样的,继续填,没能填到塔顶
                    return false; // 直接返回
                }
                return dfs(i - 1, 0);
            }

            for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
                pyramid[i][j] = top;
                if (dfs(i, j + 1)) {
                    return true;
                }
            }
            return false;
        };

        return dfs(n - 2, 0);
    }
};

###go

func pyramidTransition(bottom string, allowed []string) bool {
groups := [6][6][]byte{}
for _, s := range allowed {
a, b := s[0]-'A', s[1]-'A'
groups[a][b] = append(groups[a][b], s[2])
}

n := len(bottom)
pyramid := make([][]byte, n)
for i := range n - 1 {
pyramid[i] = make([]byte, i+1)
}
pyramid[n-1] = []byte(bottom)

vis := map[string]struct{}{} // 访问标记

var dfs func(int, int) bool
dfs = func(i, j int) bool {
if i < 0 {
return true
}

if j == i+1 {
row := string(pyramid[i])
if _, ok := vis[row]; ok { // 这一行之前填过一模一样的,继续填,没能填到塔顶
return false // 直接返回
}
vis[row] = struct{}{}
return dfs(i-1, 0)
}

for _, top := range groups[pyramid[i+1][j]-'A'][pyramid[i+1][j+1]-'A'] {
pyramid[i][j] = top
if dfs(i, j+1) {
return true
}
}
return false
}

return dfs(n-2, 0)
}

优化二:减少更多重复搜索

lc756-3c.png{:width=600px}

###py

class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        groups = defaultdict(list)
        for s in allowed:
            groups[s[:2]].append(s[2])

        n = len(bottom)
        pyramid = [[] for _ in range(n)]
        pyramid[-1] = bottom

        vis = set()

        def dfs(i: int, j: int) -> bool:
            if i < 0:
                return True

            row = ''.join(pyramid[i])
            if row in vis:  # 之前填过一模一样的,这个局部的金字塔无法填完
                return False  # 继续递归也无法填完,直接返回

            if j == i + 1:
                vis.add(row)
                return dfs(i - 1, 0)

            for top in groups[pyramid[i + 1][j] + pyramid[i + 1][j + 1]]:
                pyramid[i].append(top)
                if dfs(i, j + 1):
                    return True
                pyramid[i].pop()
            return False

        return dfs(n - 2, 0)

###java

class Solution {
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        List<Character>[][] groups = new ArrayList[6][6];
        for (List<Character>[] row : groups) {
            Arrays.setAll(row, _ -> new ArrayList<>());
        }
        for (String S : allowed) {
            char[] s = S.toCharArray();
            groups[s[0] - 'A'][s[1] - 'A'].add(s[2]);
        }

        int n = bottom.length();
        char[][] pyramid = new char[n][];
        for (int i = 0; i < n - 1; i++) {
            pyramid[i] = new char[i + 1];
        }
        pyramid[n - 1] = bottom.toCharArray();

        Set<String> vis = new HashSet<>();

        return dfs(n - 2, 0, pyramid, vis, groups);
    }

    private boolean dfs(int i, int j, char[][] pyramid, Set<String> vis, List<Character>[][] groups) {
        if (i < 0) {
            return true;
        }

        String row = new String(pyramid[i], 0, j);
        if (vis.contains(row)) { // 之前填过一模一样的,这个局部的金字塔无法填完
            return false; // 继续递归也无法填完,直接返回
        }

        if (j == i + 1) {
            vis.add(row);
            return dfs(i - 1, 0, pyramid, vis, groups);
        }

        for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
            pyramid[i][j] = top;
            if (dfs(i, j + 1, pyramid, vis, groups)) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    bool pyramidTransition(string bottom, vector<string>& allowed) {
        string groups[6][6];
        for (auto& s : allowed) {
            groups[s[0] - 'A'][s[1] - 'A'] += s[2];
        }

        int n = bottom.size();
        vector<string> pyramid(n);
        pyramid[n - 1] = move(bottom);

        unordered_set<string> vis;

        auto dfs = [&](this auto&& dfs, int i, int j) -> bool {
            if (i < 0) {
                return true;
            }

            if (vis.contains(pyramid[i])) { // 之前填过一模一样的,这个局部的金字塔无法填完
                return false; // 继续递归也无法填完,直接返回
            }

            if (j == i + 1) {
                vis.insert(pyramid[i]);
                return dfs(i - 1, 0);
            }

            for (char top : groups[pyramid[i + 1][j] - 'A'][pyramid[i + 1][j + 1] - 'A']) {
                pyramid[i] += top;
                if (dfs(i, j + 1)) {
                    return true;
                }
                pyramid[i].pop_back();
            }
            return false;
        };

        return dfs(n - 2, 0);
    }
};

###go

func pyramidTransition(bottom string, allowed []string) bool {
groups := [6][6][]byte{}
for _, s := range allowed {
a, b := s[0]-'A', s[1]-'A'
groups[a][b] = append(groups[a][b], s[2])
}

n := len(bottom)
pyramid := make([][]byte, n)
for i := range n - 1 {
pyramid[i] = make([]byte, i+1)
}
pyramid[n-1] = []byte(bottom)

vis := map[string]struct{}{}

var dfs func(int, int) bool
dfs = func(i, j int) bool {
if i < 0 {
return true
}

row := string(pyramid[i][:j])
if _, ok := vis[row]; ok { // 之前填过一模一样的,这个局部的金字塔无法填完
return false // 继续递归也无法填完,直接返回
}

if j == i+1 {
vis[row] = struct{}{}
return dfs(i-1, 0)
}

for _, top := range groups[pyramid[i+1][j]-'A'][pyramid[i+1][j+1]-'A'] {
pyramid[i][j] = top
if dfs(i, j+1) {
return true
}
}
return false
}

return dfs(n-2, 0)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n|\Sigma|^n)$,其中 $n$ 是 $\textit{bottom}$ 的长度,$|\Sigma|=6$ 是字符集合的大小。把字符串按照长度分类,长为 $k$ 的字符串至多有 $|\Sigma|^k$ 个。等比数列求和 $|\Sigma| + |\Sigma|^2 + \cdots + |\Sigma|^{n-1} = \mathcal{O}(|\Sigma|^n)$。一共有 $\mathcal{O}(|\Sigma|^n)$ 个字符串,每个字符串花费 $\mathcal{O}(n)$ 的时间与 $\textit{vis}$ 交互(查询,插入)。
  • 空间复杂度:$\mathcal{O}(n|\Sigma|^n)$。$\textit{vis}$ 保存了 $\mathcal{O}(|\Sigma|^n)$ 个字符串,每个字符串需要 $\mathcal{O}(n)$ 的空间。

优化三:位运算

把 $\texttt{A}$ 到 $\texttt{F}$ 映射为 $1$ 到 $6$。由于每个字母只占用 $3$ 个比特位,可以用二进制数表示字符串。

由于倒数第二排最多 $5$ 个字母,所以记录到 $\textit{vis}$ 中的二进制数的长度最多为 $15$。

:为什么不映射为 $0$ 到 $5$?

:比如,我们无法区分 $\texttt{AA}$ 和 $\texttt{AAA}$,二者都是 $0$。

###py

class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        groups = [[[] for _ in range(7)] for _ in range(7)]
        for a, b, c in allowed:
            # A~F -> 1~6
            groups[ord(a) & 31][ord(b) & 31].append(ord(c) & 31)

        n = len(bottom)
        pyramid = [0] * n
        for i, ch in enumerate(bottom):
            pyramid[-1] |= (ord(ch) & 31) << (i * 3)  # 等价于 pyramid[-1][i] = ord(ch)&31

        vis = set()

        def dfs(i: int, j: int) -> bool:
            if i < 0:
                return True

            if pyramid[i] in vis:
                return False

            if j == i + 1:
                vis.add(pyramid[i])
                return dfs(i - 1, 0)

            for top in groups[pyramid[i + 1] >> (j * 3) & 7][pyramid[i + 1] >> ((j + 1) * 3) & 7]:
                pyramid[i] &= ~(7 << (j * 3))  # 清除之前填的字母,等价于 pyramid[i][j] = 0
                pyramid[i] |= top << (j * 3)  # 等价于 pyramid[i][j] = top
                if dfs(i, j + 1):
                    return True
            return False

        return dfs(n - 2, 0)

###java

class Solution {
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        List<Integer>[][] groups = new ArrayList[7][7];
        for (List<Integer>[] row : groups) {
            Arrays.setAll(row, _ -> new ArrayList<>());
        }
        for (String S : allowed) {
            char[] s = S.toCharArray();
            // A~F -> 1~6
            groups[s[0] & 31][s[1] & 31].add(s[2] & 31);
        }

        char[] s = bottom.toCharArray();
        int n = s.length;
        int[] pyramid = new int[n];
        for (int i = 0; i < n; i++) {
            pyramid[n - 1] |= (s[i] & 31) << (i * 3); // 等价于 pyramid[n-1][i] = s[i]&31
        }

        boolean[] vis = new boolean[1 << ((n - 1) * 3)];

        return dfs(n - 2, 0, pyramid, vis, groups);
    }

    private boolean dfs(int i, int j, int[] pyramid, boolean[] vis, List<Integer>[][] groups) {
        if (i < 0) {
            return true;
        }

        if (vis[pyramid[i]]) {
            return false;
        }

        if (j == i + 1) {
            vis[pyramid[i]] = true;
            return dfs(i - 1, 0, pyramid, vis, groups);
        }

        for (int top : groups[pyramid[i + 1] >> (j * 3) & 7][pyramid[i + 1] >> ((j + 1) * 3) & 7]) {
            pyramid[i] &= ~(7 << (j * 3)); // 清除之前填的字母,等价于 pyramid[i][j] = 0
            pyramid[i] |= top << (j * 3); // 等价于 pyramid[i][j] = top
            if (dfs(i, j + 1, pyramid, vis, groups)) {
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
public:
    bool pyramidTransition(string bottom, vector<string>& allowed) {
        vector<int> groups[7][7];
        for (auto& s : allowed) {
            // A~F -> 1~6
            groups[s[0] & 31][s[1] & 31].push_back(s[2] & 31);
        }

        int n = bottom.size();
        vector<int> pyramid(n);
        for (int i = 0; i < n; i++) {
            pyramid[n - 1] |= (bottom[i] & 31) << (i * 3); // 等价于 pyramid[n-1][i] = bottom[i]&31
        }

        vector<uint8_t> vis(1 << ((n - 1) * 3));

        auto dfs = [&](this auto&& dfs, int i, int j) -> bool {
            if (i < 0) {
                return true;
            }

            if (vis[pyramid[i]]) {
                return false;
            }

            if (j == i + 1) {
                vis[pyramid[i]] = true;
                return dfs(i - 1, 0);
            }

            for (int top : groups[pyramid[i + 1] >> (j * 3) & 7][pyramid[i + 1] >> ((j + 1) * 3) & 7]) {
                pyramid[i] &= ~(7 << (j * 3)); // 清除之前填的字母,等价于 pyramid[i][j] = 0
                pyramid[i] |= top << (j * 3); // 等价于 pyramid[i][j] = top
                if (dfs(i, j + 1)) {
                    return true;
                }
            }
            return false;
        };

        return dfs(n - 2, 0);
    }
};

###go

func pyramidTransition(bottom string, allowed []string) bool {
groups := [7][7][]byte{}
for _, s := range allowed {
a, b := s[0]&31, s[1]&31 // A~F -> 1~6
groups[a][b] = append(groups[a][b], s[2]&31)
}

n := len(bottom)
pyramid := make([]int, n)
for i, ch := range bottom {
pyramid[n-1] |= int(ch&31) << (i * 3) // 等价于 pyramid[n-1][i] = ch&31
}

vis := make([]bool, 1<<((n-1)*3))

var dfs func(int, int) bool
dfs = func(i, j int) bool {
if i < 0 {
return true
}

if vis[pyramid[i]] {
return false
}

if j == i+1 {
vis[pyramid[i]] = true
return dfs(i-1, 0)
}

for _, top := range groups[pyramid[i+1]>>(j*3)&7][pyramid[i+1]>>((j+1)*3)&7] {
pyramid[i] &^= 7 << (j * 3) // 清除之前填的字母,等价于 pyramid[i][j] = 0
pyramid[i] |= int(top) << (j * 3) // 等价于 pyramid[i][j] = top
if dfs(i, j+1) {
return true
}
}
return false
}

return dfs(n-2, 0)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(|\Sigma|^n)$,其中 $n$ 是 $\textit{bottom}$ 的长度,$|\Sigma|=6$ 是字符集合的大小。
  • 空间复杂度:$\mathcal{O}(|\Sigma|^n)$。

专题训练

见下面回溯题单的「§4.7 搜索」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

一个可以炸掉目前几乎所有题解的测试用例,以及可以过的程序

"ABBBBBBG"
["BDD","BFB","FDA","CCC","FBD","FBG","EEB","GBA","CFB","ECB","GDA","ECC","EFE","FCA","DBA","FAD","BEE","GEC","CFC","CFD","BCG","CAC","FCC","GAC","CAG","CFF","BEC","EAD","GBE","FEG","BDC","GBF","FBB","FBE","DDG","FDF","EDF","FAF","BEB","DDE","EEA","FAG","GDC","DFE","CDB","DFD","GDD","DAF","ECE","DBG","DDD","EFG","FAC","FEC","GED","DEE","CCF","EFD","FDE","GDF","ECG","DAE","FCG","DCA","EAF","EED","EEE","FEA","BFG","EAA","GDE","CEB","FAE","BDE","EBB","GFD","DBB","BFD","EFF","DAD","CDC","DCD","FFD","DBD","EFA","BCB","ECF","CFA","GFF","CFG","EEG","DAG","CAE","FED","CCA","EDE","FDG","BEF","FDD","BEG","FFE","DDC","BCE","GAA","GFE","FFF","CFE","BDF","DFG","DEF","FEF","GEE","EAB","GDB","CEE","BDA","DEG","DEC","GCG","DFB","EFB","GAB","CDF","CAF","BAB","GCC","DDA","CDD","FCB","DAA","GBG","BAC","GCE","GAG","EAE","EBG","GBD","BFE","CCD","EDD","FCD","FBC","DEB","EDG","GEA","EBE","GDG","DBE","BCD","GBB","FEB","DED","CAD","EDB","CED","GCD","BFC","GAE","CEG","EEC","FEE","GCA","FCF","ECD","DCC","BDB","EBD","DCE","BAE","FFG","DDF","DCF","BED","CAA","BCF","GBC","CCB","BCC","GFC","FFB","DFA","BBA","BBB","BBC","BBD","BBE","BBF","BBG","AAA","ABA","ACA","ADA","AEA","AFA","BGG","CGG","DGG","EGG","FGG","GGG"]

回溯法能过的唯一原因是数据太弱。

上面的测试用例的构造思路是这样的:形如A*的如AA,AB,AC...,但除AG以外,都只能放A,形如*G如BG,CG,...,但除AG以外,都只能放G。AG上没有能放的字母。其他组合上面能放几乎所有字母。

可以看出来对这个例子来说,每一层最左边只能是A,最右边只能是G,倒数第二层只能是AG,而AG上不能放字母,因此无解。但是所有的解都是在最后一步才失败的,因此遍历过的状态数极其巨大。

以官方题解为例,官方题解主要包括朴素的回溯和按行去重两种方法。朴素回溯的复杂度是O(A^(N^2)),而非题解说的O(A^N),对于这个测试例来说,无论C++还是Java都会炸,Python就更不用说了。

按行去重也就是官方Java题解中用的方法,遇到整行的时候会缓存整行的结果,这可以将复杂度降至O(A^(2N)),差的这一点可以让Java通过(Java用位运算hash的方式常数也比较低),但是官方题解的Python即使打开seen的hash也会炸。

能通过的方法是将中途所有的状态都保存下来去重。每次尝试在下一层两个字母上堆一个字母的时候,下一层有一个字母就被压到底部了,它与后续求解无关,因此当前状态永远可以用上一层已经堆上的字母 + 下一层仍然露出来的字母来表示,总的状态数是O(NA^N),正好在可以通过的范围内。

###python3

from functools import lru_cache


class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        trans = {}
        for p in allowed:
            trans.setdefault(p[:2], []).append(p[2])

        @lru_cache(None)
        def search(a, b):
            if len(a) == 2:
                if not b:
                    return a in trans
                else:
                    return any(search(b + t, '') for t in trans.get(a, []))
            else:
                return any(search(a[1:], b + t) for t in trans.get(a[:2], []))

        return search(bottom, '')

事实上这并不是最强的测试例,以下测试例耗时会更长:

"ADDDDDDA"
["ADA","ADB","ADC","AEA","AEB","AEC","AFA","AFB","AFC","AGA","AGB","AGC","BDA","BDB","BDC","BEA","BEB","BEC","BFA","BFB","BFC","BGA","BGB","BGC","CDA","CDB","CDC","CEA","CEB","CEC","CFA","CFB","CFC","CGA","CGB","CGC","DAA","DAB","DAC","DBA","DBB","DBC","DCA","DCB","DCC","EAA","EAB","EAC","EBA","EBB","EBC","ECA","ECB","ECC","FAA","FAB","FAC","FBA","FBB","FBC","FCA","FCB","FCC","GAA","GAB","GAC","GBA","GBB","GBC","GCA","GCB","GCC","DDA","DDB","DDC","DEA","DEB","DEC","DFA","DFB","DFC","DGA","DGB","DGC","EDA","EDB","EDC","EEA","EEB","EEC","EFA","EFB","EFC","EGA","EGB","EGC","FDA","FDB","FDC","FEA","FEB","FEC","FFA","FFB","FFC","FGA","FGB","FGC","GDA","GDB","GDC","GEA","GEB","GEC","GFA","GFB","GFC","GGA","GGB","GGC","DDD","DDE","DDF","DDG","DED","DEE","DEF","DEG","DFD","DFE","DFF","DFG","DGD","DGE","DGF","DGG","EDD","EDE","EDF","EDG","EED","EEE","EEF","EEG","EFD","EFE","EFF","EFG","EGD","EGE","EGF","EGG","FDD","FDE","FDF","FDG","FED","FEE","FEF","FEG","FFD","FFE","FFF","FFG","FGD","FGE","FGF","FGG","GDD","GDE","GDF","GDG","GED","GEE","GEF","GEG","GFD","GFE","GFF","GFG","GGD","GGE","GGF","GGG"]

"ADDDDDDA"
["GDF","EFB","FFC","DCB","GFD","GCD","BGA","EEC","DEC","EDD","CDA","FEF","CGD","ECA","DGD","FFD","BEA","GEE","EDE","CED","GEF","BCA","CFB","GGA","GGB","FDE","FGE","GAB","FBA","FFA","ECF","FDA","CCD","CGA","DED","EBB","FFG","BFB","FCB","DGE","DDB","BEB","GDG","FDB","FFB","CGB","GFE","FEG","FDC","CDF","GCE","DFD","DFG","CCA","GFA","FCE","BDA","ACA","CDB","GDE","EEA","FBB","FEC","DCA","FAB","CCE","GGF","CAA","EEF","CDD","FGA","DDC","CGC","DFF","CEE","EDC","FCA","GBB","CEC","DEE","DEB","AFB","EEG","FGB","FCD","GFC","GCA","DDD","FGC","ECC","FDF","GEC","DBB","DFB","EED","GAA","FGD","DBA","DCD","EBA","FCF","ECG","CDE","DCE","EAA","DAB","FDD","DFC","GGD","BGB","CBA","GDC","AGB","BFA","EFD","EGA","FAA","GEG","EAB","EDA","DEA","CEA","DCC","GCG","DAA","ECD","GFB","GBA","GCF","EEE","AFA","EGC","AGA","FEB","CCF","DGA","FFF","CCB","DDA","BCB","ADA","ECB","DGG","EGE","CFC","GCC","FGF","FDG","CGF","DDE","AEB","GGG","CCC","DCG","DDG","CEF","FGG","ACB","DFE","BDB","CCG","CGG","CFG","DGC","EFC","GDB","FEE","EDB","CFF","EFF","FEA","EFE","GEA","CDC","EDF","CGE","CFA","GGC","DEG","FCC","FFE","CFD","EDG","CAB","CEB","GDA","EGB","ECE","GCB","DGF","DGB","EGF","EGG","CEG","GFG","ADB","EFA","GFF","DEF","GEB"]

"AGGGGGGA"
["AFA","AFB","AFC","AFD","AFE","AGA","AGB","AGC","AGD","AGE","BFA","BFB","BFC","BFD","BFE","BGA","BGB","BGC","BGD","BGE","CFA","CFB","CFC","CFD","CFE","CGA","CGB","CGC","CGD","CGE","DFA","DFB","DFC","DFD","DFE","DGA","DGB","DGC","DGD","DGE","EFA","EFB","EFC","EFD","EFE","EGA","EGB","EGC","EGD","EGE","FAA","FAB","FAC","FAD","FAE","FBA","FBB","FBC","FBD","FBE","FCA","FCB","FCC","FCD","FCE","FDA","FDB","FDC","FDD","FDE","FEA","FEB","FEC","FED","FEE","GAA","GAB","GAC","GAD","GAE","GBA","GBB","GBC","GBD","GBE","GCA","GCB","GCC","GCD","GCE","GDA","GDB","GDC","GDD","GDE","GEA","GEB","GEC","GED","GEE","FFA","FFB","FFC","FFD","FFE","FGA","FGB","FGC","FGD","FGE","GFA","GFB","GFC","GFD","GFE","GGA","GGB","GGC","GGD","GGE","FFF","FFG","FGF","FGG","GFF","GFG","GGF","GGG"]

这两个测试例的构造思路是,将字母分成两类,一类是Type-0,一类是Type-1。0和0上不能放数字,0和1、1和0上只能放0类数字,而1和1上能放全部数字。然后给出的串两侧是Type-0字母,中间全部是Type-1字母。区别在于第一个使用ABC作为Type-0,第二个使用ABCD,第三个使用ABCDE。别看ABCDE时的allow数组似乎最短,但它对前面的算法威胁是最大的,因为最前面的测试例每一行两侧的字母固定是A和G,而这个例子中两侧的字母可以选择的数量更多,每一行中中间可以使用所有字母,两侧只能用Type-0,因此最后一个测试例的状态数是文章最开始测试例状态数的25倍。

这三个测试例可以炸掉LeetCode的标程,让标程超时(标程大概就是官方题解中记忆化的Java版本)。最后一个测试例可以炸掉上面的Python答案(如果改用Java或C++估计可以通过)。

在前面答案的基础上进行提前剪枝,可以减少状态数,通过这几个测试例:

###python3

from functools import lru_cache


class Solution:
    def pyramidTransition(self, bottom: str, allowed: List[str]) -> bool:
        trans = {}
        for p in allowed:
            trans.setdefault(p[:2], []).append(p[2])

        @lru_cache(None)
        def search(a, b):
            if len(b) >= 2:
                if not search(b, ''):
                    return False
            if len(a) == 2:
                if not b:
                    return a in trans
                else:
                    return any(search(b + t, '') for t in trans.get(a, []))
            else:
                return any(search(a[1:], b + t) for t in trans.get(a[:2], []))

        return search(bottom, '')

注意到在处理(a,b)时,提前判断了(b,'')是否可行。这个判断对题目最开始的测试例没有太大帮助,但对后面这几个测试例有一定作用,主要是有效防止枚举出Type-0字符。目前没有已知的测试例可以fail这个程序了。

30行递递递递递递递递递递递递归

解题思路

1.首先为allowed数组建立一个哈希表,键为下面的两块砖头,值为上面的砖头。注意,同样的下方砖头可能允许不同的上方砖头(例如ABC和ABD),故哈希表的值用vector
2.递归函数gogogo接受三个参数,第一个bottom为目前检查的层,upper为已经形成的部分下一层,pos为目前bottom已经检查到了第几块砖头。
3.gogogo中,若pos == bottom.size()-1,说明这一层已经检查完毕啦,故将upper设为新的待检查层,注意哦,若upper.size()为1,说明已经到顶啦,返回true即可。
4.若下一个位置不存在可以放置的砖块,即vc.empty(),则返回false。否则,尝试所有可以放置的砖块。

代码

###cpp

class Solution {
public:
    unordered_map<string, vector<char>> mp;       
    bool pyramidTransition(string bottom, vector<string>& allowed) {
        for(string str:allowed){
            mp[str.substr(0, 2)].push_back(str.back());
        }
        string upper = "";
        return gogogo(bottom, upper, 0);
    }
    
    bool gogogo(string bottom, string upper, int pos){
        if(pos == bottom.size()-1){
            if(upper.size() == 1) return true;
            bottom = upper;
            upper = "";
            pos = 0;
        }
        vector<char>& vc= mp[bottom.substr(pos, 2)];
        if(vc.empty()) return false;
        else{
            for(int i = 0; i < vc.size(); i++){
                if(gogogo(bottom, upper + vc[i], pos+1)) return true;
            }
        }
        return false;
    }
};

每日一题-统计有序矩阵中的负数🟢

给你一个 m * n 的矩阵 grid,矩阵中的元素无论是按行还是按列,都以非严格递减顺序排列。 请你统计并返回 grid 中 负数 的数目。

 

示例 1:

输入:grid = [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]]
输出:8
解释:矩阵中共有 8 个负数。

示例 2:

输入:grid = [[3,2],[1,0]]
输出:0

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 100
  • -100 <= grid[i][j] <= 100

 

进阶:你可以设计一个时间复杂度为 O(n + m) 的解决方案吗?

【图解】疯狂撕数字,一图秒懂(Python/Java/C++/C/Go/JS/Rust)

lc1351-2c.png{:width=600px}

注:也可以从左下角开始,方法类似。

总结:利用 $\textit{grid}$ 行列有序的性质,我们可以用 $\mathcal{O}(1)$ 的时间获取 $\mathcal{O}(m)$ 或 $\mathcal{O}(n)$ 的信息。相比之下,$\mathcal{O}(mn)$ 的暴力查找(一个一个地找),每次花费 $\mathcal{O}(1)$ 的时间,只获取了 $\mathcal{O}(1)$ 的信息。

这就是为什么我们可以把 $\mathcal{O}(mn)$ 的暴力查找优化成 $\mathcal{O}(m+n)$。

###py

class Solution:
    def countNegatives(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        ans = 0
        i, j = 0, n - 1  # 从右上角开始
        while i < m and j >= 0:  # 还有剩余元素
            if grid[i][j] < 0:
                ans += m - i  # 这一列剩余元素都是负数
                j -= 1
            else:
                i += 1  # 这一行剩余元素全都非负,排除
        return ans

###java

class Solution {
    public int countNegatives(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int ans = 0;
        int i = 0;
        int j = n - 1; // 从右上角开始
        while (i < m && j >= 0) { // 还有剩余元素
            if (grid[i][j] < 0) {
                ans += m - i; // 这一列剩余元素都是负数
                j--;
            } else {
                i++; // 这一行剩余元素全都非负,排除
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int countNegatives(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int ans = 0;
        int i = 0, j = n - 1; // 从右上角开始
        while (i < m && j >= 0) { // 还有剩余元素
            if (grid[i][j] < 0) {
                ans += m - i; // 这一列剩余元素都是负数
                j--;
            } else {
                i++; // 这一行剩余元素全都非负,排除
            }
        }
        return ans;
    }
};

###c

int countNegatives(int** grid, int gridSize, int* gridColSize) {
    int m = gridSize, n = gridColSize[0];
    int ans = 0;
    int i = 0, j = n - 1; // 从右上角开始
    while (i < m && j >= 0) { // 还有剩余元素
        if (grid[i][j] < 0) {
            ans += m - i; // 这一列剩余元素都是负数
            j--;
        } else {
            i++; // 这一行剩余元素全都非负,排除
        }
    }
    return ans;
}

###go

func countNegatives(grid [][]int) (ans int) {
m, n := len(grid), len(grid[0])
i, j := 0, n-1 // 从右上角开始
for i < m && j >= 0 { // 还有剩余元素
if grid[i][j] < 0 {
ans += m - i // 这一列剩余元素都是负数
j--
} else {
i++ // 这一行剩余元素全都非负,排除
}
}
return
}

###js

var countNegatives = function(grid) {
    const m = grid.length, n = grid[0].length;
    let ans = 0;
    let i = 0, j = n - 1; // 从右上角开始
    while (i < m && j >= 0) { // 还有剩余元素
        if (grid[i][j] < 0) {
            ans += m - i; // 这一列剩余元素都是负数
            j--;
        } else {
            i++; // 这一行剩余元素全都非负,排除
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn count_negatives(grid: Vec<Vec<i32>>) -> i32 {
        let m = grid.len();
        let n = grid[0].len();
        let mut ans = 0;
        let mut i = 0;
        let mut j = n - 1; // 从右上角开始
        while i < m && j < n { // 还有剩余元素
            if grid[i][j] < 0 {
                ans += m - i; // 这一列剩余元素都是负数
                j -= 1;
            } else {
                i += 1; // 这一行剩余元素全都非负,排除
            }
        }
        ans as _
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(m + n)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(1)$。

:本题不存在时间复杂度低于 $\mathcal{O}(m + n)$ 的算法,见 240 题我的题解 文末的注。

相似题目

240. 搜索二维矩阵 II

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌