阅读视图

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

每日一题-执行操作后元素的最高频率 II🔴

给你一个整数数组 nums 和两个整数 k 和 numOperations 。

你必须对 nums 执行 操作  numOperations 次。每次操作中,你可以:

  • 选择一个下标 i ,它在之前的操作中 没有 被选择过。
  • nums[i] 增加范围 [-k, k] 中的一个整数。

在执行完所有操作以后,请你返回 nums 中出现 频率最高 元素的出现次数。

一个元素 x 的 频率 指的是它在数组中出现的次数。

 

示例 1:

输入:nums = [1,4,5], k = 1, numOperations = 2

输出:2

解释:

通过以下操作得到最高频率 2 :

  • 将 nums[1] 增加 0 ,nums 变为 [1, 4, 5] 。
  • 将 nums[2] 增加 -1 ,nums 变为 [1, 4, 4] 。

示例 2:

输入:nums = [5,11,20,20], k = 5, numOperations = 1

输出:2

解释:

通过以下操作得到最高频率 2 :

  • 将 nums[1] 增加 0 。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 0 <= k <= 109
  • 0 <= numOperations <= nums.length

两种方法:差分/三指针+双指针(Python/Java/C++/Go)

方法一:差分

前置知识差分原理讲解

设 $x = \textit{nums}[i]$。根据题意,$x$ 可以变成 $[x-k,x+k]$ 中的整数。

题目让我们计算操作后,最多有多少个数相同。

例如 $\textit{nums}=[2,4]$,$k=1$,$\textit{numOperations}=2$。$2$ 可以变成 $[1,3]$ 中的整数,$4$ 可以变成 $[3,5]$ 中的整数。$2$ 和 $4$ 都可以变成 $3$,所以答案是 $2$。

一般地,$x$ 可以变成 $[x-k,x+k]$ 中的整数,我们可以把 $[x-k,x+k]$ 中的每个整数的出现次数都加一,然后统计出现次数的最大值。这可以用差分实现。

计算差分的前缀和。设有 $\textit{sumD}$ 个数可以变成 $y$。

如果 $y$ 不在 $\textit{nums}$ 中,那么 $y$ 的最大出现次数不能超过 $\textit{numOperations}$,与 $\textit{sumD}$ 取最小值,得 $\min(\textit{sumD}, \textit{numOperations})$。

如果 $y$ 在 $\textit{nums}$ 中,且出现了 $\textit{cnt}$ 次,那么有 $\textit{sumD}-\textit{cnt}$ 个其他元素(不等于 $y$ 的数)可以变成 $y$,但这不能超过 $\textit{numOperations}$,所以有

$$
\min(\textit{sumD}-\textit{cnt}, \textit{numOperations})
$$

个其他元素可以变成 $y$,再加上 $y$ 自身出现的次数 $\textit{cnt}$,得到 $y$ 的最大频率为

$$
\textit{cnt} + \min(\textit{sumD}-\textit{cnt}, \textit{numOperations}) = \min(\textit{sumD}, \textit{cnt}+\textit{numOperations})
$$

注意上式兼容 $y$ 不在 $\textit{nums}$ 中的情况,此时 $\textit{cnt}=0$。

本题视频讲解,欢迎点赞关注~

答疑

:为什么代码只考虑在 $\textit{diff}$ 和 $\textit{nums}$ 中的数字?

:比如 $x$ 在 $\textit{diff}$ 中,$x+1,x+2,\ldots$ 不在 $\textit{diff}$ 中,那么 $x+1,x+2,\ldots$ 的 $\textit{sumD}$ 和 $\textit{x}$ 的是一样的,无需重复计算。此外,要想算出比 $\min(\textit{sumD}, \textit{cnt}+\textit{numOperations})$ 更大的数,要么 $\textit{sumD}$ 变大,要么 $\textit{cnt}$ 变大。「变大」时的 $x$ 必然在 $\textit{diff}$ 或 $\textit{nums}$ 中。

###py

class Solution:
    def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
        cnt = defaultdict(int)
        diff = defaultdict(int)
        for x in nums:
            cnt[x] += 1
            diff[x]  # 把 x 插入 diff,以保证下面能遍历到 x
            diff[x - k] += 1  # 把 [x-k,x+k] 中的每个整数的出现次数都加一
            diff[x + k + 1] -= 1

        ans = sum_d = 0
        for x, d in sorted(diff.items()):
            sum_d += d
            ans = max(ans, min(sum_d, cnt[x] + numOperations))
        return ans

###java

class Solution {
    int maxFrequency(int[] nums, int k, int numOperations) {
        Map<Integer, Integer> cnt = new HashMap<>();
        Map<Integer, Integer> diff = new TreeMap<>();
        for (int x : nums) {
            cnt.merge(x, 1, Integer::sum); // cnt[x]++
            diff.putIfAbsent(x, 0); // 把 x 插入 diff,以保证下面能遍历到 x
            // 把 [x-k, x+k] 中的每个整数的出现次数都加一
            diff.merge(x - k, 1, Integer::sum); // diff[x-k]++
            diff.merge(x + k + 1, -1, Integer::sum); // diff[x+k+1]--
        }

        int ans = 0;
        int sumD = 0;
        for (Map.Entry<Integer, Integer> e : diff.entrySet()) {
            sumD += e.getValue();
            ans = Math.max(ans, Math.min(sumD, cnt.getOrDefault(e.getKey(), 0) + numOperations));
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxFrequency(vector<int>& nums, int k, int numOperations) {
        unordered_map<int, int> cnt;
        map<int, int> diff;
        for (int x : nums) {
            cnt[x]++;
            diff[x]; // 把 x 插入 diff,以保证下面能遍历到 x
            diff[x - k]++; // 把 [x-k, x+k] 中的每个整数的出现次数都加一
            diff[x + k + 1]--;
        }

        int ans = 0, sum_d = 0;
        for (auto& [x, d] : diff) {
            sum_d += d;
            ans = max(ans, min(sum_d, cnt[x] + numOperations));
        }
        return ans;
    }
};

###go

func maxFrequency(nums []int, k, numOperations int) (ans int) {
cnt := map[int]int{}
diff := map[int]int{}
for _, x := range nums {
cnt[x]++
diff[x] += 0 // 把 x 插入 diff,以保证下面能遍历到 x
diff[x-k]++  // 把 [x-k, x+k] 中的每个整数的出现次数都加一
diff[x+k+1]--
}

sumD := 0
for _, x := range slices.Sorted(maps.Keys(diff)) {
sumD += diff[x]
ans = max(ans, min(sumD, cnt[x]+numOperations))
}
return
}

复杂度分析

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

方法二:同向三指针 + 同向双指针

核心思路

  1. 计算有多少个数能变成 $x$,其中 $x = \textit{nums}[i]$。用同向三指针实现。
  2. 计算有多少个数能变成 $x$,其中 $x$ 不一定在 $\textit{nums}$ 中。用同向双指针实现。

同向三指针

把 $\textit{nums}$ 从小到大排序。

遍历 $\textit{nums}$。设 $x=\textit{nums}[i]$,计算元素值在 $[x-k,x+k]$ 中的元素个数,这些元素都可以变成 $x$。

遍历的同时,维护左指针 $\textit{left}$,它是最小的满足

$$
\textit{nums}[\textit{left}] \ge x - k
$$

的下标。

遍历的同时,维护右指针 $\textit{right}$,它是最小的满足

$$
\textit{nums}[\textit{right}] > x + k
$$

的下标。如果不存在,则 $\textit{right}=n$。

下标在左闭右开区间 $[\textit{left},\textit{right})$ 中的元素,都可以变成 $x$。这有

$$
\textit{sumD} = \textit{right} - \textit{left}
$$

个。

遍历的同时,求出 $x$ 有 $\textit{cnt}$ 个。然后用方法一的公式,更新答案的最大值。

同向双指针

同向三指针没有考虑「变成不在 $\textit{nums}$ 中的数」这种情况。

然而,不在 $\textit{nums}$ 中的数太多了!怎么减少计算量?

假设都变成 $y$,那么只有 $[y-k,y+k]$ 中的数能变成 $y$。

把 $\textit{nums}[i]$ 视作一维坐标轴上的点,想象一个窗口 $[y-k,y+k]$ 在不断地向右滑动,什么时候窗口内的元素个数才会变大?

  • 如果我们从 $[y-k-1,y+k-1]$ 移动到 $[y-k,y+k]$,且 $y+k$ 不在 $\textit{nums}$ 中,此时窗口内的元素个数不会变大,甚至因为左端点收缩了,元素个数可能会变小。
  • 所以,只有当 $y+k$ 恰好在 $\textit{nums}$ 中时,窗口内的元素个数才可能会变大。
  • 结论:我们只需考虑 $y+k$ 在 $\textit{nums}$ 中时的 $y$!

于是,枚举 $x=\textit{nums}[\textit{right}]$,计算元素值在 $[x-2k,x]$ 中的元素个数,这些元素都可以变成同一个数 $y=x-k$。

左指针 $\textit{left}$ 是最小的满足

$$
\textit{nums}[\textit{left}] \ge x-2k
$$

的下标。

计算好 $\textit{left}$ 后,下标在 $[\textit{left}, \textit{right}]$ 中的数可以变成一样的,这有

$$
\textit{right} - \textit{left} + 1
$$

个。注意上式不能超过 $\textit{numOperations}$。

小优化

由于同向双指针算出的结果不超过 $\textit{numOperations}$,所以当同向三指针计算完毕后,如果发现答案已经 $\ge \textit{numOperations}$,那么无需计算同向双指针。

###py

class Solution:
    def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
        nums.sort()

        n = len(nums)
        ans = cnt = left = right = 0
        for i, x in enumerate(nums):
            cnt += 1
            # 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if i < n - 1 and x == nums[i + 1]:
                continue
            while nums[left] < x - k:
                left += 1
            while right < n and nums[right] <= x + k:
                right += 1
            ans = max(ans, min(right - left, cnt + numOperations))
            cnt = 0

        if ans >= numOperations:
            return ans

        left = 0
        for right, x in enumerate(nums):
            while nums[left] < x - k * 2:
                left += 1
            ans = max(ans, right - left + 1)
        return min(ans, numOperations)  # 最后和 numOperations 取最小值

###java

class Solution {
    public int maxFrequency(int[] nums, int k, int numOperations) {
        Arrays.sort(nums);

        int n = nums.length;
        int ans = 0;
        int cnt = 0;
        int left = 0;
        int right = 0;
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            cnt++;
            // 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if (i < n - 1 && x == nums[i + 1]) {
                continue;
            }
            while (nums[left] < x - k) {
                left++;
            }
            while (right < n && nums[right] <= x + k) {
                right++;
            }
            ans = Math.max(ans, Math.min(right - left, cnt + numOperations));
            cnt = 0;
        }

        if (ans >= numOperations) {
            return ans;
        }

        left = 0;
        for (right = 0; right < n; right++) {
            int x = nums[right];
            while (nums[left] < x - k * 2) {
                left++;
            }
            ans = Math.max(ans, right - left + 1);
        }
        return Math.min(ans, numOperations); // 最后和 numOperations 取最小值
    }
}

###cpp

class Solution {
public:
    int maxFrequency(vector<int>& nums, int k, int numOperations) {
        ranges::sort(nums);

        int n = nums.size();
        int ans = 0, cnt = 0, left = 0, right = 0;
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            cnt++;
            // 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if (i < n - 1 && x == nums[i + 1]) {
                continue;
            }
            while (nums[left] < x - k) {
                left++;
            }
            while (right < n && nums[right] <= x + k) {
                right++;
            }
            ans = max(ans, min(right - left, cnt + numOperations));
            cnt = 0;
        }

        if (ans >= numOperations) {
            return ans;
        }

        left = 0;
        for (int right = 0; right < n; right++) {
            int x = nums[right];
            while (nums[left] < x - k * 2) {
                left++;
            }
            ans = max(ans, right - left + 1);
        }
        return min(ans, numOperations); // 最后和 numOperations 取最小值
    }
};

###go

func maxFrequency(nums []int, k, numOperations int) (ans int) {
slices.Sort(nums)

n := len(nums)
var cnt, left, right int
for i, x := range nums {
cnt++
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n-1 && x == nums[i+1] {
continue
}
for nums[left] < x-k {
left++
}
for right < n && nums[right] <= x+k {
right++
}
ans = max(ans, min(right-left, cnt+numOperations))
cnt = 0
}

if ans >= numOperations {
return ans
}

left = 0
for right, x := range nums {
for nums[left] < x-k*2 {
left++
}
ans = max(ans, right-left+1)
}
return min(ans, numOperations) // 最后和 numOperations 取最小值
}

也可以把两个 for 循环合起来。

###py

class Solution:
    def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
        nums.sort()

        n = len(nums)
        ans = cnt = left = right = left2 = 0
        for i, x in enumerate(nums):
            while nums[left2] < x - k * 2:
                left2 += 1
            ans = max(ans, min(i - left2 + 1, numOperations))

            cnt += 1
            # 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if i < n - 1 and x == nums[i + 1]:
                continue
            while nums[left] < x - k:
                left += 1
            while right < n and nums[right] <= x + k:
                right += 1
            ans = max(ans, min(right - left, cnt + numOperations))
            cnt = 0

        return ans

###java

class Solution {
    public int maxFrequency(int[] nums, int k, int numOperations) {
        Arrays.sort(nums);

        int n = nums.length;
        int ans = 0;
        int cnt = 0;
        int left = 0;
        int right = 0;
        int left2 = 0;
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            while (nums[left2] < x - k * 2) {
                left2++;
            }
            ans = Math.max(ans, Math.min(i - left2 + 1, numOperations));

            cnt++;
            // 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if (i < n - 1 && x == nums[i + 1]) {
                continue;
            }
            while (nums[left] < x - k) {
                left++;
            }
            while (right < n && nums[right] <= x + k) {
                right++;
            }
            ans = Math.max(ans, Math.min(right - left, cnt + numOperations));
            cnt = 0;
        }

        return ans;
    }
}

###cpp

class Solution {
public:
    int maxFrequency(vector<int>& nums, int k, int numOperations) {
        ranges::sort(nums);

        int n = nums.size();
        int ans = 0, cnt = 0, left = 0, right = 0, left2 = 0;
        for (int i = 0; i < n; i++) {
            int x = nums[i];
            while (nums[left2] < x - k * 2) {
                left2++;
            }
            ans = max(ans, min(i - left2 + 1, numOperations));

            cnt++;
            // 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
            if (i < n - 1 && x == nums[i + 1]) {
                continue;
            }
            while (nums[left] < x - k) {
                left++;
            }
            while (right < n && nums[right] <= x + k) {
                right++;
            }
            ans = max(ans, min(right - left, cnt + numOperations));
            cnt = 0;
        }

        return ans;
    }
};

###go

func maxFrequency(nums []int, k, numOperations int) (ans int) {
slices.Sort(nums)

n := len(nums)
var cnt, left, right, left2 int
for i, x := range nums {
for nums[left2] < x-k*2 {
left2++
}
ans = max(ans, min(i-left2+1, numOperations))

cnt++
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n-1 && x == nums[i+1] {
continue
}
for nums[left] < x-k {
left++
}
for right < n && nums[right] <= x+k {
right++
}
ans = max(ans, min(right-left, cnt+numOperations))
cnt = 0
}

return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

  1. 下面数据结构题单的「§2.1 一维差分」。
  2. 下面双指针题单的「§3.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自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

枚举

解法:枚举

如果最后的众数是 $x$,那么答案就是 $c_x + \min(m, \sum\limits_{x - k \le i \le x + k, i \ne x} c_i)$,其中 $c_x$ 是 nums 里 $x$ 的数量,$m$ 是操作次数。

这个式子的值会随着 $x$ 的变化发生怎样的改变?$c_x$ 肯定在 $x$ 取到 nums 里有的数时才尽可能大,而 $\sum\limits_{x - k \le i \le x + k, i \ne x} c_i$ 的值和 $x$ 被几个数的“范围”覆盖到有关。一个数 $x$ 能覆盖的范围是 $[x - k, x + k]$,当 $x$ 增加到某个区间的左端点时,被覆盖到的数量会增加 $1$,之后在区间中间不发生变化。

所以我们只要考虑 $x$ 取 nums 中出现的数,以及它们 $-k$,共 $2n$ 种数。$\sum$ 的值可以用二分的方式快速计算。复杂度 $\mathcal{O}(n\log n)$。

参考代码(c++)

###cpp

class Solution {
public:
    int maxFrequency(vector<int>& nums, int k, int numOperations) {
        // 把所有要考虑的值放进 set 里
        unordered_set<int> st;
        // 统计 nums 里每种数出现了几次
        unordered_map<int, int> mp;
        for (int x : nums) {
            st.insert(x); st.insert(x - k);
            mp[x]++;
        }

        // 给 nums 排序,方便接下来二分计数。
        sort(nums.begin(), nums.end());
        int ans = 0;
        for (int x : st) {
            int tmp = 0;
            // 二分计算 (x, x + k] 里有几个数
            tmp += upper_bound(nums.begin(), nums.end(), x + k) - upper_bound(nums.begin(), nums.end(), x);
            // 二分计算 [x - k, x) 里有几个数
            tmp += lower_bound(nums.begin(), nums.end(), x) - lower_bound(nums.begin(), nums.end(), x - k);
            tmp = min(tmp, numOperations);
            ans = max(ans, mp[x] + tmp);
        }
        return ans;
    }
};

每日一题-执行操作后字典序最小的字符串🟡

给你一个字符串 s 以及两个整数 ab 。其中,字符串 s 的长度为偶数,且仅由数字 09 组成。

你可以在 s 上按任意顺序多次执行下面两个操作之一:

  • 累加:将  a 加到 s 中所有下标为奇数的元素上(下标从 0 开始)。数字一旦超过 9 就会变成 0,如此循环往复。例如,s = "3456"a = 5,则执行此操作后 s 变成 "3951"
  • 轮转:将 s 向右轮转 b 位。例如,s = "3456"b = 1,则执行此操作后 s 变成 "6345"

请你返回在 s 上执行上述操作任意次后可以得到的 字典序最小 的字符串。

如果两个字符串长度相同,那么字符串 a 字典序比字符串 b 小可以这样定义:在 ab 出现不同的第一个位置上,字符串 a 中的字符出现在字母表中的时间早于 b 中的对应字符。例如,"0158” 字典序比 "0190" 小,因为不同的第一个位置是在第三个字符,显然 '5' 出现在 '9' 之前。

 

示例 1:

输入:s = "5525", a = 9, b = 2
输出:"2050"
解释:执行操作如下:
初态:"5525"
轮转:"2555"
累加:"2454"
累加:"2353"
轮转:"5323"
累加:"5222"
累加:"5121"
轮转:"2151"
累加:"2050"
无法获得字典序小于 "2050" 的字符串。

示例 2:

输入:s = "74", a = 5, b = 1
输出:"24"
解释:执行操作如下:
初态:"74"
轮转:"47"
累加:"42"
轮转:"24"
无法获得字典序小于 "24" 的字符串。

示例 3:

输入:s = "0011", a = 4, b = 2
输出:"0011"
解释:无法获得字典序小于 "0011" 的字符串。

 

提示:

  • 2 <= s.length <= 100
  • s.length 是偶数
  • s 仅由数字 09 组成
  • 1 <= a <= 9
  • 1 <= b <= s.length - 1

枚举轮转到最左边的下标 + 裴蜀定理(Python/Java/C++/C/Go/JS/Rust)

:题干中的「数字一旦超过 $9$ 就会变成 $0$」的意思是,数字 $x$ 加上 $a$ 后,会变成 $(x+a)\bmod 10$。

不轮转

从特殊到一般,先考虑只累加、不轮转的情况。

为了让字典序尽量小,第一个奇数下标 $1$ 上的数字 $s_1$,越小越好。一旦我们确定了 $s_1$ 的最终值,就确定了一共累加的值。由于所有奇数下标都要累加同一个数,所以也就确定了其余奇数下标的值。

那么,$s_1$ 最小可以是多少?可以是 $0$ 吗?

比如 $s_1 = 5$,$a=2$。不断累加 $a$,$s_1$ 的变化情况如下:

$$
5\to 7\to 9\to 1\to 3\to 5\to \cdots
$$

这种情况 $s_1$ 只能是奇数,最小是 $1$。

而如果 $s_1 = 5$,$a=3$,$s_1$ 的变化情况如下:

$$
5\to 8\to 1\to 4\to 7\to 0\to 3\to 6\to 9\to 2\to 5 \cdots
$$

这种情况 $s_1$ 可以变成 $[0,9]$ 中的任意整数,最小是 $0$。

一般地,设累加操作执行了 $k\ (k\ge 0)$ 次,那么 $s_1$ 变成

$$
r = (s_1 + ak)\bmod 10
$$

也就是 $s_1 + ak$ 减去若干个 $10$ 等于 $r$,即

$$
s_1 + ak - 10q = r
$$

变形得

$$
ak - 10q = r-s_1
$$

裴蜀定理 指出,该方程有解,当且仅当 $r-s_1$ 是 $g = \gcd(a,10)$ 的倍数,即

$$
r \equiv s_1 \pmod g
$$

其中 $\equiv$ 是同余符号,详细解释见 模运算的世界:当加减乘除遇上取模

上式表明,$s_1$ 通过累加操作变成的数,必须与 $s_1$ 关于模 $g$ 同余,所以 $s_1$ 可以变成的最小值为

$$
s_1\bmod g
$$

从 $s_1$ 到 $s_1\bmod g$,一共要累加的值为

$$
s_1\bmod g - s_1 + 10
$$

其中 $+10$ 保证减法结果非负。

枚举轮转到最左边的下标

例如 $s=\texttt{012345}$,$b=4$,执行轮转操作,得到

$$
\texttt{012345}\to\texttt{234501}\to\texttt{450123}\to\texttt{012345}\to\cdots
$$

只有 $s_0,s_2,s_4$ 可以轮转到最左边。

类似上文的思路,根据裴蜀定理,可以轮转到最左边的下标,必须是 $\textit{step} = \gcd(b,n)$ 的倍数,其中 $n$ 是 $s$ 的长度。

枚举 $i = 0,\textit{step},2\cdot \textit{step}, 3\cdot \textit{step},\dots$ 作为轮转到最左边的下标。

分类讨论:

  • 如果 $\gcd(b,n)$ 是偶数,无论如何轮转,我们只能对奇数下标执行累加操作。
  • 如果 $\gcd(b,n)$ 是奇数,轮转一次后,原来的偶数下标变成奇数下标。可以先轮转一次,执行累加,再轮转到我们想要的位置。可以视作我们拥有了「对偶数下标执行累加操作」的能力。

###py

class Solution:
    def findLexSmallestString(self, s: str, a: int, b: int) -> str:
        s = list(map(int, s))
        n = len(s)
        step = gcd(b, n)
        g = gcd(a, 10)
        ans = [inf]

        def modify(start: int) -> None:
            ch = t[start]  # 最靠前的数字,越小越好
            # ch 可以变成的最小值为 ch%g
            # 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
            # 从 ch 到 ch%g,需要增加 inc(循环中会 %10 保证结果在 [0,9] 中)
            inc = ch % g - ch
            if inc:  # 优化:inc 为 0 时,t[j] 不变,无需执行 for 循环
                for j in range(start, n, 2):
                    t[j] = (t[j] + inc) % 10

        for i in range(0, n, step):      
            t = s[i:] + s[:i]  # 轮转
            modify(1)  # 累加操作(所有奇数下标)
            if step % 2:  # 能对偶数下标执行累加操作
                modify(0)  # 累加操作(所有偶数下标)
            ans = min(ans, t)

        return ''.join(map(str, ans))

###java

class Solution {
    public String findLexSmallestString(String S, int a, int b) {
        char[] s = S.toCharArray();
        int n = s.length;
        char[] t = new char[n];
        int step = gcd(b, n);
        int g = gcd(a, 10);
        String ans = null;

        for (int i = 0; i < n; i += step) {
            // t = s[i,n) + s[0,i)
            System.arraycopy(s, i, t, 0, n - i);
            System.arraycopy(s, 0, t, n - i, i);

            modify(t, 1, g); // 累加操作(所有奇数下标)
            if (step % 2 > 0) { // 能对偶数下标执行累加操作
                modify(t, 0, g); // 累加操作(所有偶数下标)
            }

            String str = new String(t);
            if (ans == null || str.compareTo(ans) < 0) {
                ans = str;
            }
        }

        return ans;
    }

    private void modify(char[] t, int start, int g) {
        int ch = t[start] - '0'; // 最靠前的数字,越小越好
        // ch 可以变成的最小值为 ch%g
        // 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
        // 从 ch 到 ch%g,需要增加 inc,其中 +10 保证 inc 非负(循环中会 %10 保证结果在 [0,9] 中)
        int inc = ch % g - ch + 10;
        for (int j = start; j < t.length; j += 2) {
            t[j] = (char) ('0' + (t[j] - '0' + inc) % 10);
        }
    }

    private int gcd(int a, int b) {
        while (a != 0) {
            int tmp = a;
            a = b % a;
            b = tmp;
        }
        return b;
    }
}

###cpp

class Solution {
public:
    string findLexSmallestString(string s, int a, int b) {
        int n = s.size();
        int step = gcd(b, n);
        int g = gcd(a, 10);
        string ans;

        for (int i = 0; i < n; i += step) {
            string t = s.substr(i) + s.substr(0, i); // 轮转

            auto modify = [&](int start) -> void {
                int ch = t[start] - '0'; // 最靠前的数字,越小越好
                // ch 可以变成的最小值为 ch%g
                // 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
                // 从 ch 到 ch%g,需要增加 inc,其中 +10 保证 inc 非负(循环中会 %10 保证结果在 [0,9] 中)
                int inc = ch % g - ch + 10;
                for (int j = start; j < n; j += 2) {
                    t[j] = '0' + (t[j] - '0' + inc) % 10;
                }
            };

            modify(1); // 累加操作(所有奇数下标)
            if (step % 2) { // 能对偶数下标执行累加操作
                modify(0); // 累加操作(所有偶数下标)
            }

            if (ans.empty() || t < ans) {
                ans = move(t);
            }
        }

        return ans;
    }
};

###c

int gcd(int a, int b) {
    while (a) {
        int tmp = a;
        a = b % a;
        b = tmp;
    }
    return b;
}

void modify(char* t, int n, int start, int g) {
    int ch = t[start] - '0'; // 最靠前的数字,越小越好
    // ch 可以变成的最小值为 ch%g
    // 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
    // 从 ch 到 ch%g,需要增加 inc,其中 +10 保证 inc 非负(循环中会 %10 保证结果在 [0,9] 中)
    int inc = ch % g - ch + 10;
    for (int j = start; j < n; j += 2) {
        t[j] = '0' + (t[j] - '0' + inc) % 10;
    }
}

char* findLexSmallestString(char* s, int a, int b) {
    int n = strlen(s);
    int step = gcd(b, n);
    int g = gcd(a, 10);

    char* ans = malloc((n + 1) * sizeof(char));
    ans[0] = CHAR_MAX;
    ans[1] = '\0';

    char* t = malloc((n + 1) * sizeof(char));
    t[n] = '\0';

    for (int i = 0; i < n; i += step) {
        // t = s[i,n) + s[0,i)
        strncpy(t, s + i, n - i);
        strncpy(t + n - i, s, i);

        modify(t, n, 1, g); // 累加操作(所有奇数下标)
        if (step % 2) { // 能对偶数下标执行累加操作
            modify(t, n, 0, g); // 累加操作(所有偶数下标)
        }

        if (strcmp(t, ans) < 0) {
            strcpy(ans, t);
        }
    }

    free(t);
    return ans;
}

###go

func findLexSmallestString(s string, a int, b int) string {
n := len(s)
step := gcd(b, n)
g := gcd(a, 10)
var ans []byte

for i := 0; i < n; i += step {
t := []byte(s[i:] + s[:i]) // 轮转
modify := func(start int) {
ch := t[start] - '0' // 最靠前的数字,越小越好
// ch 可以变成的最小值为 ch%g
// 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
// 从 ch 到 ch%g,需要增加 inc,其中 +10 保证 inc 非负(循环中会 %10 保证结果在 [0,9] 中)
inc := ch%byte(g) + 10 - ch
for j := start; j < n; j += 2 {
t[j] = '0' + (t[j]-'0'+inc)%10
}
}
modify(1) // 累加操作(所有奇数下标)
if step%2 > 0 { // 能对偶数下标执行累加操作
modify(0) // 累加操作(所有偶数下标)
}
if ans == nil || bytes.Compare(t, ans) < 0 {
ans = t
}
}

return string(ans)
}

func gcd(a, b int) int {
for a != 0 {
a, b = b%a, a
}
return b
}

###js

var findLexSmallestString = function(s, a, b) {
    const arr = s.split('').map(ch => parseInt(ch));
    const n = arr.length;
    const step = gcd(b, n);
    const g = gcd(a, 10);
    let ans = null;

    function modify(t, start) {
        const ch = t[start]; // 最靠前的数字,越小越好
        // ch 可以变成的最小值为 ch%g
        // 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
        // 从 ch 到 ch%g,需要增加 inc(循环中会 %10 保证结果在 [0,9] 中)
        let inc = ch % g - ch + 10;
        if (inc === 0) { // 优化:inc 为 0 时,t[j] 不变,无需执行 for 循环
            return;
        }
        for (let j = start; j < n; j += 2) {
            t[j] = (t[j] + inc) % 10;
        }
    }

    for (let i = 0; i < n; i += step) {
        const t = arr.slice(i).concat(arr.slice(0, i)); // 轮转
        modify(t, 1); // 累加操作(所有奇数下标)
        if (step % 2) { // 能对偶数下标执行累加操作
            modify(t, 0); // 累加操作(所有偶数下标)
        }
        if (ans === null || compareArray(t, ans) < 0) {
            ans = t;
        }
    }

    return ans.join('');
};

function gcd(a, b) {
    while (a) {
        [a, b] = [b % a, a];
}
return b;
}

function compareArray(a, b) {
    const n = a.length;
    for (let i = 0; i < n; i++) {
        if (a[i] !== b[i]) {
            return a[i] - b[i];
        }
    }
    return 0;
}

###rust

impl Solution {
    pub fn find_lex_smallest_string(s: String, a: i32, b: i32) -> String {
        let n = s.len();
        let step = gcd(b, n as i32) as usize;
        let g = gcd(a, 10) as u8;
        let mut ans = vec![u8::MAX];

        let modify = |t: &mut [u8], start: usize| {
            let ch = t[start] - b'0'; // 最靠前的数字,越小越好
            // ch 可以变成的最小值为 ch%g
            // 例如 ch=5,g=2,那么 ch+2+2+2(模 10)后变成 1,不可能变得更小
            // 从 ch 到 ch%g,需要增加 inc,其中 +10 保证 inc 非负(循环中会 %10 保证结果在 [0,9] 中)
            let inc = ch % g + 10 - ch;
            for j in (start..n).step_by(2) {
                t[j] = b'0' + (t[j] - b'0' + inc) % 10;
            }
        };

        for i in (0..n).step_by(step) {
            let mut t = format!("{}{}", &s[i..], &s[..i]).into_bytes(); // 轮转
            modify(&mut t, 1); // 累加操作(所有奇数下标)
            if step % 2 != 0 { // 能对偶数下标执行累加操作
                modify(&mut t, 0); // 累加操作(所有偶数下标)
            }
            ans = ans.min(t);
        }

        unsafe { String::from_utf8_unchecked(ans) }
    }
}

fn gcd(mut a: i32, mut b: i32) -> i32 {
    while a != 0 {
        (a, b) = (b % a, a);
    }
    b
}

复杂度分析

  • 时间复杂度:$\mathcal{O}\left(\dfrac{n^2}{\gcd(b,n)}\right)$,其中 $n$ 是 $s$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

:还可以枚举奇数下标累加值,枚举偶数下标累加值,然后用类似 最小表示法 的思想,计算在固定累加值的情况下,轮转后的最小字典序。时间复杂度 $\mathcal{O}(D^2n)$,$D=10$。

分类题单

如何科学刷题?

  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站@灵茶山艾府

[Python3/Java/C++/Go] 一题双解:BFS 暴力模拟 & 枚举

方法一:BFS

本题数据规模较小,我们可以考虑使用 BFS 暴力搜索所有可能的状态,然后取字典序最小的状态即可。

我们定义队列 $q$,初始时将字符串 $s$ 入队,定义一个哈希表 $vis$,用于记录字符串是否已经出现过,另外定义一个字符串 $ans$,用于记录答案。

然后,我们不断从队列中取出字符串,将其与答案 $ans$ 进行比较,如果当前字符串字典序更小,则更新答案。然后我们对该字符串进行累加和轮转操作,得到新的字符串,如果新的字符串没有出现过,则将其入队,并更新 $vis$。一直重复上述操作,直到队列为空。

class Solution:
    def findLexSmallestString(self, s: str, a: int, b: int) -> str:
        q = deque([s])
        vis = {s}
        ans = s
        while q:
            s = q.popleft()
            if ans > s:
                ans = s
            t1 = ''.join([str((int(c) + a) % 10) if i & 1 else c for i, c in enumerate(s)])
            t2 = s[-b:] + s[:-b]
            for t in (t1, t2):
                if t not in vis:
                    vis.add(t)
                    q.append(t)
        return ans
class Solution {
    public String findLexSmallestString(String s, int a, int b) {
        Deque<String> q = new ArrayDeque<>();
        q.offer(s);
        Set<String> vis = new HashSet<>();
        vis.add(s);
        String ans = s;
        int n = s.length();
        while (!q.isEmpty()) {
            s = q.poll();
            if (ans.compareTo(s) > 0) {
                ans = s;
            }
            char[] cs = s.toCharArray();
            for (int i = 1; i < n; i += 2) {
                cs[i] = (char) (((cs[i] - '0' + a) % 10) + '0');
            }
            String t1 = String.valueOf(cs);
            String t2 = s.substring(n - b) + s.substring(0, n - b);
            for (String t : List.of(t1, t2)) {
                if (vis.add(t)) {
                    q.offer(t);
                }
            }
        }
        return ans;
    }
}
class Solution {
public:
    string findLexSmallestString(string s, int a, int b) {
        queue<string> q{{s}};
        unordered_set<string> vis{{s}};
        string ans = s;
        int n = s.size();
        while (!q.empty()) {
            s = q.front();
            q.pop();
            ans = min(ans, s);
            string t1 = s;
            for (int i = 1; i < n; i += 2) {
                t1[i] = (t1[i] - '0' + a) % 10 + '0';
            }
            string t2 = s.substr(n - b) + s.substr(0, n - b);
            for (auto& t : {t1, t2}) {
                if (!vis.count(t)) {
                    vis.insert(t);
                    q.emplace(t);
                }
            }
        }
        return ans;
    }
};
func findLexSmallestString(s string, a int, b int) string {
q := []string{s}
vis := map[string]bool{s: true}
ans := s
n := len(s)
for len(q) > 0 {
s = q[0]
q = q[1:]
if ans > s {
ans = s
}
t1 := []byte(s)
for i := 1; i < n; i += 2 {
t1[i] = byte((int(t1[i]-'0')+a)%10 + '0')
}
t2 := s[n-b:] + s[:n-b]
for _, t := range []string{string(t1), t2} {
if !vis[t] {
vis[t] = true
q = append(q, t)
}
}
}
return ans
}

方法二:枚举

我们观察发现,对于累加操作,数字最多累加 $10$ 次,就会回到原来的状态;对于轮转操作,字符串最多轮转 $n$ 次,也会回到原来的状态。

因此,轮转操作最多产生 $n$ 种状态,如果轮转位数 $b$ 为偶数,累加操作只会对奇数位数字产生影响,因此总共产生 $n \times 10$ 种状态;如果轮转位数 $b$ 为奇数,累加操作既会对奇数位数字产生影响,也会对偶数位数字产生影响,因此总共产生 $n \times 10 \times 10$ 种状态。

所以,我们直接枚举所有的字符串状态,取字典序最小的状态即可。

class Solution:
    def findLexSmallestString(self, s: str, a: int, b: int) -> str:
        ans = s
        n = len(s)
        s = list(s)
        for _ in range(n):
            s = s[-b:] + s[:-b]
            for j in range(10):
                for k in range(1, n, 2):
                    s[k] = str((int(s[k]) + a) % 10)
                if b & 1:
                    for p in range(10):
                        for k in range(0, n, 2):
                            s[k] = str((int(s[k]) + a) % 10)
                        t = ''.join(s)
                        if ans > t:
                            ans = t
                else:
                    t = ''.join(s)
                    if ans > t:
                        ans = t
        return ans
class Solution {
    public String findLexSmallestString(String s, int a, int b) {
        int n = s.length();
        String ans = s;
        for (int i = 0; i < n; ++i) {
            s = s.substring(b) + s.substring(0, b);
            char[] cs = s.toCharArray();
            for (int j = 0; j < 10; ++j) {
                for (int k = 1; k < n; k += 2) {
                   cs[k] = (char) (((cs[k] - '0' + a) % 10) + '0');
                }
                if ((b & 1) == 1) {
                    for (int p = 0; p < 10; ++p) {
                        for (int k = 0; k < n; k += 2) {
                            cs[k] = (char) (((cs[k] - '0' + a) % 10) + '0');
                        }
                        s = String.valueOf(cs);
                        if (ans.compareTo(s) > 0) {
                            ans = s;
                        }
                    }
                } else {
                    s = String.valueOf(cs);
                    if (ans.compareTo(s) > 0) {
                        ans = s;
                    }
                }
            }
        }
        return ans;
    }
}
class Solution {
public:
    string findLexSmallestString(string s, int a, int b) {
        int n = s.size();
        string ans = s;
        for (int i = 0; i < n; ++i) {
            s = s.substr(n - b) + s.substr(0, n - b);
            for (int j = 0; j < 10; ++j) {
                for (int k = 1; k < n; k += 2) {
                    s[k] = (s[k] - '0' + a) % 10 + '0';
                }
                if (b & 1) {
                    for (int p = 0; p < 10; ++p) {
                        for (int k = 0; k < n; k += 2) {
                            s[k] = (s[k] - '0' + a) % 10 + '0';
                        }
                        ans = min(ans, s);
                    }
                } else {
                    ans = min(ans, s);
                }
            }
        }
        return ans;
    }
};
func findLexSmallestString(s string, a int, b int) string {
n := len(s)
ans := s
for _ = range s {
s = s[n-b:] + s[:n-b]
cs := []byte(s)
for j := 0; j < 10; j++ {
for k := 1; k < n; k += 2 {
cs[k] = byte((int(cs[k]-'0')+a)%10 + '0')
}
if b&1 == 1 {
for p := 0; p < 10; p++ {
for k := 0; k < n; k += 2 {
cs[k] = byte((int(cs[k]-'0')+a)%10 + '0')
}
s = string(cs)
if ans > s {
ans = s
}
}
} else {
s = string(cs)
if ans > s {
ans = s
}
}
}
}
return ans
}

时间复杂度 $O(n^2 \times 10^2)$,空间复杂度 $O(n)$。其中 $n$ 为字符串 $s$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

暴力美学(更新进一步优化方法,0ms/6.5MB)

本场周赛题解 | 我的LeetCode比赛题解

解题思路

在本题的数据范围内,可以枚举所有可能的操作结果,从中选择最小的那一个。关键是:如何枚举?

首先考虑轮转操作。对于一个长度为$N$的字符串,每次轮转$b$个位置,等价于轮转$g=GCD(N,b)$个位置。所以,我们只需要以$g$为步长进行轮转的枚举即可。

接下来考虑增加操作。如果$g$是偶数,那么不管怎么轮转,我们只能对下标为奇数的位置进行增加操作;否则,我们也可以对下标为偶数的位置进行增加操作。根据这两种情况,枚举奇数和偶数位置的增加操作的次数即可。因为每一位是$[0,9]$之间的数,而$1\leq a\leq9$,所以我们只需要枚举操作$[0,9]$次的情形。

复杂度

  • 时间复杂度$O(D^2|S|^2)$,本题中$D=10$。
  • 空间复杂度$O(|S|)$

代码

###cpp

class Solution {
    int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }
public:
    string findLexSmallestString(string s, int a, int b) {
        int n = s.size();
        string ans = s;
        string t = s + s;
        int g = gcd(n, b);
        for (int i = 0; i < n; i += g) {
            string p = t.substr(i, n);
            for (int j = 0; j <= 9; ++j) {
                int th = g % 2 == 0 ? 0 : 9;
                for (int k = 0; k <= th; ++k) {
                    string q(p);
                    for (int t = 1; t < n; t += 2)
                        q[t] = '0' + (q[t] - '0' + a * j) % 10;
                    for (int t = 0; t < n; t += 2)
                        q[t] = '0' + (q[t] - '0' + a * k) % 10;
                    ans = min(ans, q);
                }
            }
        }
        return ans;
    }
};

进一步优化

截屏2020-10-19 21.00.22.png

上述方法还有进一步优化的空间吗?答案是肯定的。事实上,对于每一个轮转,我们只需要让其第一个奇数位,也即p[1]达到最小;当然,如果可以对偶数位进行操作,则需要再考虑让p[0]达到最小。这样,对奇偶的讨论就变成了并联而非串联的关系。在确定了奇数位(和偶数位)的操作次数后,对于每一轮转,我们只会生成一个唯一的新字符串。

从而,我们将算法的时间复杂度优化到了$O(|S|^2+D)$。

###cpp

class Solution {
    int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }
public:
    string findLexSmallestString(string s, int a, int b) {
        int n = s.size();
        string ans = s;
        string t = s + s;
        int ga = gcd(10, a), gb = gcd(n, b);
        
        // 奇偶通用的add操作
        auto add = [&](string &p, int pos) {
            int lo = p[pos] - '0', added = 0;
            for (int i = ga; i < 10; i += ga) {
                int c = (p[pos] - '0' + i) % 10;
                if (c < lo) {
                    lo = c;
                    added = i;
                }
            }
            if (added)
                for (int i = pos; i < n; i += 2)
                    p[i] = '0' + (p[i] - '0' + added) % 10;
        };
        
        // rotate操作
        for (int i = 0; i < n; i += gb) {
            string p = t.substr(i, n);
            add(p, 1);
            if (gb % 2)
                add(p, 0);
            ans = min(ans, p);
        }
        return ans;
    }
};

[Python3/Java/C++/Go/TypeScript] 一题一解:贪心 + 排序(清晰题解)

方法一:贪心 + 排序

我们不妨对数组 $\textit{nums}$ 排序,然后从左到右考虑每个元素 $x$。

对于第一个元素,我们可以贪心地将其变为 $x - k$,这样可以使得 $x$ 尽可能小,给后续的元素留下更多的空间。我们用变量 $\textit{pre}$ 当前使用到的元素的最大值,初始化为负无穷大。

对于后续的元素 $x$,我们可以贪心地将其变为 $\min(x + k, \max(x - k, \textit{pre} + 1))$。这里的 $\max(x - k, \textit{pre} + 1)$ 表示我们尽可能地将 $x$ 变得更小,但不能小于 $\textit{pre} + 1$,如果存在该值,且小于 $x + k$,我们就可以将 $x$ 变为该值,不重复元素数加一,然后我们更新 $\textit{pre}$ 为该值。

遍历结束,我们就得到了不重复元素的最大数量。

###python

class Solution:
    def maxDistinctElements(self, nums: List[int], k: int) -> int:
        nums.sort()
        ans = 0
        pre = -inf
        for x in nums:
            cur = min(x + k, max(x - k, pre + 1))
            if cur > pre:
                ans += 1
                pre = cur
        return ans

###java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length;
        int ans = 0, pre = Integer.MIN_VALUE;
        for (int x : nums) {
            int cur = Math.min(x + k, Math.max(x - k, pre + 1));
            if (cur > pre) {
                ++ans;
                pre = cur;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxDistinctElements(vector<int>& nums, int k) {
        ranges::sort(nums);
        int ans = 0, pre = INT_MIN;
        for (int x : nums) {
            int cur = min(x + k, max(x - k, pre + 1));
            if (cur > pre) {
                ++ans;
                pre = cur;
            }
        }
        return ans;
    }
};

###go

func maxDistinctElements(nums []int, k int) (ans int) {
sort.Ints(nums)
pre := math.MinInt32
for _, x := range nums {
cur := min(x+k, max(x-k, pre+1))
if cur > pre {
ans++
pre = cur
}
}
return
}

###ts

function maxDistinctElements(nums: number[], k: number): number {
    nums.sort((a, b) => a - b);
    let [ans, pre] = [0, -Infinity];
    for (const x of nums) {
        const cur = Math.min(x + k, Math.max(x - k, pre + 1));
        if (cur > pre) {
            ++ans;
            pre = cur;
        }
    }
    return ans;
}

时间复杂度 $O(n \times \log n)$,空间复杂度 $O(\log n)$。其中 $n$ 为数组 $\textit{nums}$ 的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈 😄~

每日一题-执行操作后不同元素的最大数量🟡

给你一个整数数组 nums 和一个整数 k

你可以对数组中的每个元素 最多 执行 一次 以下操作:

  • 将一个在范围 [-k, k] 内的整数加到该元素上。

返回执行这些操作后,nums 中可能拥有的不同元素的 最大 数量。

 

示例 1:

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

输出: 6

解释:

对前四个元素执行操作,nums 变为 [-1, 0, 1, 2, 3, 4],可以获得 6 个不同的元素。

示例 2:

输入: nums = [4,4,4,4], k = 1

输出: 3

解释:

nums[0] 加 -1,以及对 nums[1] 加 1,nums 变为 [3, 5, 4, 4],可以获得 3 个不同的元素。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 109
  • 0 <= k <= 109

3397. 执行操作后不同元素的最大数量

解法

思路和算法

为了计算不同元素的最大数量,应将数组 $\textit{nums}$ 按升序排序,然后从小到大依次考虑每个元素执行操作之后的值。

排序之后,数组 $\textit{nums}$ 中的最小元素为 $\textit{nums}[0]$,将最小元素 $\textit{nums}[0]$ 更新后的值记为 $x_0$,则根据贪心策略,$x_0$ 应取可能的最小值,即 $x_0 = \textit{nums}[0] - k$。理由如下:将所有元素执行操作之后的最小值记为 $x_0$,如果 $x_0 > \textit{nums}[0] - k$,则将 $x_0$ 的值更新为 $\textit{nums}[0] - k$ 之后,所有元素执行操作之后的最小值更小,一定不会产生新的重复元素,不同元素的数量一定不变或增加。

确定 $x_0$ 之后,将次小元素 $\textit{nums}[1]$ 更新后的值记为 $x_1$,则 $\textit{nums}[1] - k \le x_1 \le \textit{nums}[1] + k$。由于 $x_0 = \textit{nums}[0] - k$ 且 $\textit{nums}[0] \le \textit{nums}[1]$,因此 $x_1 \ge x_0$,为了使不同元素的数量最大,$x_1$ 应取范围 $[\textit{nums}[1] - k, \textit{nums}[1] + k]$ 中的大于等于 $x_0 + 1$ 的最小值。理由如下。

  1. 如果 $x_1 = x_0$,则不同元素的数量不变,只有当 $x_1 > x_0$ 时才能使不同元素的数量增加。

  2. 如果 $x_1$ 的值大于可能的最小值,则将 $x_1$ 的值更新为可能的最小值之后,其余元素的取值范围更大,因此不同元素的最大数量不变或增加,不可能减少。

因此 $x_1 = \min(\max(\textit{nums}[1] - k, x_0 + 1), \textit{nums}[1] + k)$。

确定 $x_0$ 和 $x_1$ 之后,数组 $\textit{nums}$ 中的其余元素执行操作之后的值可以使用相同的方法确定。计算得到数组 $\textit{nums}$ 中的所有元素执行操作之后的值,即可得到不同元素的最大数量。

代码

###Java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        int distinct = 0;
        Arrays.sort(nums);
        int prev = Integer.MIN_VALUE;
        for (int num : nums) {
            int curr = Math.min(Math.max(num - k, prev + 1), num + k);
            if (curr > prev) {
                distinct++;
                prev = curr;
            }
        }
        return distinct;
    }
}

###C#

public class Solution {
    public int MaxDistinctElements(int[] nums, int k) {
        int distinct = 0;
        Array.Sort(nums);
        int prev = int.MinValue;
        foreach (int num in nums) {
            int curr = Math.Min(Math.Max(num - k, prev + 1), num + k);
            if (curr > prev) {
                distinct++;
                prev = curr;
            }
        }
        return distinct;
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。排序的时间是 $O(n \log n)$,排序之后需要遍历数组一次。

  • 空间复杂度:$O(\log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。排序的递归调用栈空间是 $O(\log n)$。

从小到大贪心(Python/Java/C++/Go)

一个现实中的例子:

  • 军训的某一天,同学们在操场上。现在教官吹响了口哨,同学们集合,排成一排。对于最靠左的同学 $A$,他需要尽量往左移动,给其他同学腾出位置。$A$ 旁边的同学,可以紧挨着 $A$。依此类推。

把 $\textit{nums}$ 视作 $n$ 个同学在一维数轴中的位置,从最左边的同学($\textit{nums}$ 的最小值)开始思考。

设最左边的同学的位置为 $a$,他尽量往左移,位置变成 $a-k$。

$\textit{nums}$ 的次小值 $b$ 呢?这位同学也尽量往左移:

  • 比如 $a=4,b=6,k=3$,那么 $a$ 变成 $a-k=1$,$b$ 变成 $b-k=3$。
  • 比如 $a=4,b=4,k=3$,那么 $a$ 变成 $a'=a-k=1$,$b$ 变成 $b-k=1$ 就和 $a'$ 一样了,可以稍微大一点(紧挨着 $a'$),把 $b$ 变成 $a'+1=2$。

一般地,$b$ 变成

$$
\max(b-k,a'+1)
$$

但这不能超过 $b+k$,所以最终要变成

$$
\min(\max(b-k,a'+1),b+k)
$$

相当于让 $a'+1$ 落在 $[b-k,b+k]$ 中,若超出范围则修正。

第三小的数也同理,通过前一个数可以算出当前元素能变成多少。

最后答案为 $\textit{nums}$ 中的不同元素个数。我们可以在修改的同时统计,如果发现当前元素修改后的值,比上一个元素修改后的值大,那么答案加一。

为方便计算,把 $\textit{nums}$ 从小到大排序。排序后,从左到右遍历数组,就相当于从最左边的人开始计算了。

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def maxDistinctElements(self, nums: List[int], k: int) -> int:
        nums.sort()
        ans = 0
        pre = -inf  # 记录每个人左边的人的位置
        for x in nums:
            x = min(max(x - k, pre + 1), x + k)
            if x > pre:
                ans += 1
                pre = x
        return ans

###java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        Arrays.sort(nums);
        int ans = 0;
        int pre = Integer.MIN_VALUE; // 记录每个人左边的人的位置
        for (int x : nums) {
            x = Math.min(Math.max(x - k, pre + 1), x + k);
            if (x > pre) {
                ans++;
                pre = x;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxDistinctElements(vector<int>& nums, int k) {
        ranges::sort(nums);
        int ans = 0;
        int pre = INT_MIN; // 记录每个人左边的人的位置
        for (int x : nums) {
            x = clamp(pre + 1, x - k, x + k); // min(max(x - k, pre + 1), x + k)
            if (x > pre) {
                ans++;
                pre = x;
            }
        }
        return ans;
    }
};

###go

func maxDistinctElements(nums []int, k int) (ans int) {
slices.Sort(nums)
pre := math.MinInt // 记录每个人左边的人的位置
for _, x := range nums {
x = min(max(x-k, pre+1), x+k)
if x > pre {
ans++
pre = x
}
}
return
}

优化

什么情况下,可以直接返回 $n$?

先考虑 $\textit{nums}$ 所有元素都相同的情况(同学们都挤在一起)。我们可以把元素 $x$ 变成 $[x-k,x+k]$ 中的整数,这一共有 $2k+1$ 个。如果 $2k+1 \ge n$,就可以让所有元素互不相同。

如果 $\textit{nums}$ 有不同元素,当 $2k+1 \ge n$ 时,更加可以让所有元素互不相同。

所以只要 $2k+1 \ge n$,就可以直接返回 $n$。

###py

class Solution:
    def maxDistinctElements(self, nums: List[int], k: int) -> int:
        if k * 2 + 1 >= len(nums):
            return len(nums)

        nums.sort()
        ans = 0
        pre = -inf  # 记录每个人左边的人的位置
        for x in nums:
            x = min(max(x - k, pre + 1), x + k)
            if x > pre:
                ans += 1
                pre = x
        return ans

###java

class Solution {
    public int maxDistinctElements(int[] nums, int k) {
        int n = nums.length;
        if (k * 2 + 1 >= n) {
            return n;
        }

        Arrays.sort(nums);
        int ans = 0;
        int pre = Integer.MIN_VALUE; // 记录每个人左边的人的位置
        for (int x : nums) {
            x = Math.min(Math.max(x - k, pre + 1), x + k);
            if (x > pre) {
                ans++;
                pre = x;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxDistinctElements(vector<int>& nums, int k) {
        int n = nums.size();
        if (k * 2 + 1 >= n) {
            return n;
        }

        ranges::sort(nums);
        int ans = 0;
        int pre = INT_MIN; // 记录每个人左边的人的位置
        for (int x : nums) {
            x = clamp(pre + 1, x - k, x + k); // min(max(x - k, pre + 1), x + k)
            if (x > pre) {
                ans++;
                pre = x;
            }
        }
        return ans;
    }
};

###go

func maxDistinctElements(nums []int, k int) (ans int) {
n := len(nums)
if k*2+1 >= n {
return n
}

slices.Sort(nums)
pre := math.MinInt // 记录每个人左边的人的位置
for _, x := range nums {
x = min(max(x-k, pre+1), x+k)
if x > pre {
ans++
pre = x
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

专题训练

见下面贪心题单的「§1.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站@灵茶山艾府

每日一题-执行操作后的最大 MEX🟡

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

在一步操作中,你可以对 nums 中的任一元素加上或减去 value

  • 例如,如果 nums = [1,2,3]value = 2 ,你可以选择 nums[0] 减去 value ,得到 nums = [-1,2,3]

数组的 MEX (minimum excluded) 是指其中数组中缺失的最小非负整数。

  • 例如,[-1,2,3] 的 MEX 是 0 ,而 [1,0,3] 的 MEX 是 2

返回在执行上述操作 任意次 后,nums 的最大 MEX

 

示例 1:

输入:nums = [1,-10,7,13,6,8], value = 5
输出:4
解释:执行下述操作可以得到这一结果:
- nums[1] 加上 value 两次,nums = [1,0,7,13,6,8]
- nums[2] 减去 value 一次,nums = [1,0,2,13,6,8]
- nums[3] 减去 value 两次,nums = [1,0,2,3,6,8]
nums 的 MEX 是 4 。可以证明 4 是可以取到的最大 MEX 。

示例 2:

输入:nums = [1,-10,7,13,6,8], value = 7
输出:2
解释:执行下述操作可以得到这一结果:
- nums[2] 减去 value 一次,nums = [1,-10,0,13,6,8]
nums 的 MEX 是 2 。可以证明 2 是可以取到的最大 MEX 。

 

提示:

  • 1 <= nums.length, value <= 105
  • -109 <= nums[i] <= 109

3ms,晚上突然想到的解法

Problem: 6321. 执行操作后的最大 MEX

[TOC]

思路

思路看代码,我就举个例子

比如数组为[1,-10,7,13,6,8],value=5,变为正数并取余之后为[1,0,2,3,1,3],对应的flag数组为[1,2,1,2,0],找到的出现次数最少的数的频率,用min记录为0,此时的num为数组的索引4,所以最终返回为4

比如数组为[1,-10,7,13,6,8],value=7,变为正数并取余之后为[1,4,0,6,6,1],对应的flag数组为[1,2,0,0,1,0,2],找到的出现次数最少的数的频率,用min记录为0,此时的num为数组的索引2,所以最终返回为2

Code

###Java


class Solution {
    public int findSmallestInteger(int[] nums, int value) {
        //flag只用来保存取余之后的值的个数,也就是[0,value-1]
        int[] flag=new int[value];
        //暂存nums[i]用
        int temp;
        //遍历nums数组,将每个数数都和value取余,然后存放在flag中
        for(int i=0;i<nums.length;i++){
           temp=nums[i];
           //当nums[i]<0的时候,有两种情况
           //一种是value的倍数,取余直接为0,不能和不为0的合并,因为0+value直接溢出flag数组了
           //第二种取余不为0,直接加上value就行
           //当nums[i]>0的时候,直接取余就好
           //当nums[i]==0的时候,直接记录在flag中
           if(temp<0){
               if(temp%value==0){
                   temp=0;
               }else{
                   temp=temp%value+value;
               }
           }else if(temp>0){
               temp=temp%value;
           }
           flag[temp]++;
        }
        int num=0;
        int min=Integer.MAX_VALUE;
        //找到[0,value-1]中出现次数最少的数的频率,用min记录
        //并使用num记录出现次数最少的数
        for(int i=0;i<flag.length;i++){
           if(flag[i]<min){
               min=flag[i];
               num=i;
           }
        }
        //如果min为0,说明num就是我们要找的答案,否则num+value*min就是我们要找的答案
        if(min==0){
            return num;
        }else{
            return num+value*min;
        }

    }

}

贪心

解法:贪心

思维第一步

看到“任一元素加上或减去 value”,而且“可以操作任意次”,马上反馈出等价条件:

数 $x$ 可以变成任意满足 y mod value == x mod value 的数 $y$。

因此,我们可以把整个序列按元素取模的值分成 value 类,每一类元素都不会因为操作而变成其他类别里的元素。

思维第二步

既然我们要让“缺失的最小非负整数”最大,设第 $i$ 类里有 $k$ 个数,那么它们需要变成 $i, v + i, 2v + i, \cdots (k - 1)v + i$ 才能让 mex 尽可能大。

举个例子帮助大家理解,假设 $v = 5$,第 $2$ 类里有 $3$ 个数,那么它们需要变成 $2, 7, 12$ 才能让 mex 尽可能大。如果变成别的,比如 $2, 12, 17$,那因为缺失了 $7$(而且其他类别的数也没法变成 $7$),那 mex 至多就是 $7$ 了。如果是 $2, 7, 12$,那 mex 还有可能是 $17$,肯定这样做更优。

最终处理

既然我们已经确定了每个数都要变成什么,那完成变化以后,直接计算变化后序列的 mex 就是答案。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int v) {
        int n = nums.size();
        // vis[i] 表示变化后的序列里是否出现过元素 i
        // 答案肯定不超过 n
        // 因为最好情况就是 nums = [0, 1, ..., n - 1],这样 mex = n
        bool vis[n + 1];
        memset(vis, 0, sizeof(vis));
        
        // cnt[i] 表示第 i 类元素目前有几个
        int cnt[v];
        memset(cnt, 0, sizeof(cnt));
        for (int x : nums) {
            // t 是元素 x 所属的类别
            int t = (x % v + v) % v;
            // y 是元素 x 应该变成什么数
            int y = cnt[t] * v + t;
            if (y < n) vis[y] = true;
            cnt[t]++;
        }
        
        // 计算变化后序列的 mex
        for (int i = 0; i <= n; i++) if (!vis[i]) return i;
        return -1;
    }
};

同余分组(Python/Java/C++/C/Go/JS/Rust)

下文记 $m=\textit{value}$。

由于同一个数可以加减任意倍的 $m$,我们可以先把每个 $\textit{nums}[i]$ 变成与 $\textit{nums}[i]$ 关于模 $m$ 同余的最小非负整数,以备后用。关于同余的介绍,请看 模运算的世界:当加减乘除遇上取模

本题有负数,根据这篇文章中的公式,我们可以把每个 $\textit{nums}[i]$ 变成

$$
(\textit{nums}[i]\bmod m + m)\bmod m
$$

从而保证取模结果在 $[0,m)$ 中。

例如 $\textit{nums}=[1,-6,-4,3,5]$,$m=3$,取模后变成 $[1,0,2,0,2]$。

然后枚举答案:

  • 有没有与 $0$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $0$。
  • 有没有与 $1$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $1$。
  • 有没有与 $2$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $2$。
  • 有没有与 $3$ 关于模 $m$ 同余的数?有,我们消耗掉一个 $0$。这个取模后等于 $0$ 的数,可以继续操作,变成 $3$。
  • 有没有与 $4$ 关于模 $m$ 同余的数?也就是看是否还有 $1$,没有,那么答案等于 $4$。

怎么知道还有没有剩余元素?用一个哈希表 $\textit{cnt}$ 统计 $(\textit{nums}[i]\bmod m + m) \bmod m$ 的个数。

本题视频讲解,欢迎点赞关注~

写法一

class Solution:
    def findSmallestInteger(self, nums: List[int], m: int) -> int:
        cnt = Counter(x % m for x in nums)
        mex = 0
        while cnt[mex % m]:
            cnt[mex % m] -= 1
            mex += 1
        return mex
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        int[] cnt = new int[m];
        for (int x : nums) {
            cnt[(x % m + m) % m]++; // 保证取模结果在 [0, m) 中
        }

        int mex = 0;
        while (cnt[mex % m]-- > 0) {
            mex++;
        }
        return mex;
    }
}
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : nums) {
            cnt.merge((x % m + m) % m, 1, Integer::sum);
        }

        int mex = 0;
        while (cnt.merge(mex % m, -1, Integer::sum) >= 0) {
            mex++;
        }
        return mex;
    }
}
class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int m) {
        unordered_map<int, int> cnt;
        for (int x : nums) {
            cnt[(x % m + m) % m]++; // 保证取模结果在 [0, m) 中
        }

        int mex = 0;
        while (cnt[mex % m]-- > 0) {
            mex++;
        }
        return mex;
    }
};
int findSmallestInteger(int* nums, int numsSize, int m) {
    int* cnt = calloc(m, sizeof(int));
    for (int i = 0; i < numsSize; i++) {
        cnt[(nums[i] % m + m) % m]++; // 保证取模结果在 [0, m) 中
    }

    int mex = 0;
    while (cnt[mex % m]-- > 0) {
        mex++;
    }

    free(cnt);
    return mex;
}
func findSmallestInteger(nums []int, m int) (mex int) {
cnt := map[int]int{}
for _, x := range nums {
cnt[(x%m+m)%m]++ // 保证取模结果在 [0, m) 中
}

for cnt[mex%m] > 0 {
cnt[mex%m]--
mex++
}
return
}
var findSmallestInteger = function(nums, m) {
    const cnt = new Map();
    for (const x of nums) {
        const v = (x % m + m) % m; // 保证取模结果在 [0, m) 中
        cnt.set(v, (cnt.get(v) ?? 0) + 1);
    }

    let mex = 0;
    while ((cnt.get(mex % m) ?? 0) > 0) {
        cnt.set(mex % m, cnt.get(mex % m) - 1);
        mex++;
    }
    return mex;
};
use std::collections::HashMap;

impl Solution {
    pub fn find_smallest_integer(nums: Vec<i32>, m: i32) -> i32 {
        let mut cnt = HashMap::new();
        for x in nums {
            // 保证取模结果在 [0, m) 中
            *cnt.entry((x % m + m) % m).or_insert(0) += 1;
        }

        for mex in 0.. {
            if let Some(c) = cnt.get_mut(&(mex % m)) {
                if *c > 0 {
                    *c -= 1;
                    continue;
                }
            }
            return mex;
        }
        unreachable!()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。由于加多少个数,就只能减多少个数,所以第二个循环至多循环 $\mathcal{O}(n)$ 次。
  • 空间复杂度:$\mathcal{O}(\min(n,m))$。哈希表中至多有 $\mathcal{O}(\min(n,m))$ 个元素。

写法二

lc2598-c.png{:width=500px}

此外,把哈希表换成数组更快。

class Solution:
    def findSmallestInteger(self, nums: List[int], m: int) -> int:
        cnt = [0] * m
        for x in nums:
            cnt[x % m] += 1

        i = cnt.index(min(cnt))
        return m * cnt[i] + i
class Solution {
    public int findSmallestInteger(int[] nums, int m) {
        int[] cnt = new int[m];
        for (int x : nums) {
            cnt[(x % m + m) % m]++;
        }

        int i = 0;
        for (int j = 1; j < m; j++) {
            if (cnt[j] < cnt[i]) {
                i = j;
            }
        }

        return m * cnt[i] + i;
    }
}
class Solution {
public:
    int findSmallestInteger(vector<int>& nums, int m) {
        vector<int> cnt(m);
        for (int x : nums) {
            cnt[(x % m + m) % m]++;
        }

        int i = ranges::min_element(cnt) - cnt.begin();
        return m * cnt[i] + i;
    }
};
int findSmallestInteger(int* nums, int numsSize, int m) {
    int* cnt = calloc(m, sizeof(int));
    for (int i = 0; i < numsSize; i++) {
        cnt[(nums[i] % m + m) % m]++;
    }

    int i = 0;
    for (int j = 1; j < m; j++) {
        if (cnt[j] < cnt[i]) {
            i = j;
        }
    }

    int ans = m * cnt[i] + i;
    free(cnt);
    return ans;
}
func findSmallestInteger(nums []int, m int) int {
cnt := make([]int, m)
for _, x := range nums {
cnt[(x%m+m)%m]++
}

i := 0
for j := 1; j < m; j++ {
if cnt[j] < cnt[i] {
i = j
}
}

return m*cnt[i] + i
}
var findSmallestInteger = function(nums, m) {
    const cnt = Array(m).fill(0);
    for (const x of nums) {
        cnt[(x % m + m) % m]++;
    }

    let i = 0;
    for (let j = 1; j < m; j++) {
        if (cnt[j] < cnt[i]) {
            i = j;
        }
    }

    return m * cnt[i] + i;
};
impl Solution {
    pub fn find_smallest_integer(nums: Vec<i32>, m: i32) -> i32 {
        let mut cnt = vec![0; m as usize];
        for x in nums {
            cnt[((x % m + m) % m) as usize] += 1;
        }

        let mut i = 0;
        for j in 1..m as usize {
            if cnt[j] < cnt[i] {
                i = j;
            }
        }

        m * cnt[i] + i as i32
    }
}

复杂度分析

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

相似题目

见下面数学题单的「§1.9 同余」。

分类题单

如何科学刷题?

  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站@灵茶山艾府

检测相邻递增子数组 II

方法一:一次遍历

思路与算法

我们对数组 $\textit{nums}$ 进行一次遍历,在遍历的过程中,用 $\textit{cnt}$ 和 $\textit{precnt}$ 分别记录到当前位置严格递增的子数组的长度,以及上一个严格递增子数组的长度。

初始时,$\textit{cnt}$ 的值为 $1$,$\textit{precnt}$ 的值为 $0$。当遍历到 $\textit{nums}[i]$ 时,如果大于 $\textit{nums}[i-1]$,则 $\textit{cnt}$ 自增 $1$;否则子数组不再递增,将 $\textit{cnt}$ 的值赋予 $\textit{precnt}$,并将 $\textit{cnt}$ 置为 $1$。

满足题目要求的两个相邻子数组有两种情况:

  1. 前一个子数组属于 $\textit{precnt}$,后一个子数组属于 $\textit{cnt}$,$k$ 值为 $\min(\textit{precnt}, \textit{cnt})$。

  2. 两个子数组都属于 $\textit{cnt}$,$k$ 值为 $\lfloor \dfrac{\textit{cnt}}{2} \rfloor$,其中 $\lfloor \cdot \rfloor$ 表示向下取整。

根据这两种情况,不断更新 $k$ 的最大值即可。

代码

###C++

class Solution {
public:
    int maxIncreasingSubarrays(vector<int>& nums) {
        int n = nums.size();
        int cnt = 1, precnt = 0, ans = 0;
        for (int i = 1; i < n; ++i) {
            if (nums[i] > nums[i - 1]) {
                ++cnt;
            }
            else {
                precnt = cnt;
                cnt = 1;
            }
            ans = max(ans, min(precnt, cnt));
            ans = max(ans, cnt / 2);
        }
        return ans;
    }
};

###Python

class Solution:
    def maxIncreasingSubarrays(self, nums: List[int]) -> int:
        n = len(nums)
        cnt, precnt, ans = 1, 0, 0
        for i in range(1, n):
            if nums[i] > nums[i - 1]:
                cnt += 1
            else:
                precnt, cnt = cnt, 1
            ans = max(ans, min(precnt, cnt))
            ans = max(ans, cnt // 2)
        return ans

###C#

public class Solution {
    public int MaxIncreasingSubarrays(IList<int> nums) {
        int n = nums.Count;
        int cnt = 1, precnt = 0, ans = 0;
        for (int i = 1; i < n; ++i) {
            if (nums[i] > nums[i - 1]) {
                ++cnt;
            } else {
                precnt = cnt;
                cnt = 1;
            }
            ans = Math.Max(ans, Math.Min(precnt, cnt));
            ans = Math.Max(ans, cnt / 2);
        }
        return ans;
    }
}

###Go

func maxIncreasingSubarrays(nums []int) int {
    n := len(nums)
    cnt, precnt, ans := 1, 0, 0
    for i := 1; i < n; i++ {
        if nums[i] > nums[i-1] {
            cnt++
        } else {
            precnt = cnt
            cnt = 1
        }
        ans = max(ans, min(precnt, cnt))
        ans = max(ans, cnt/2)
    }
    return ans
}

###Python

class Solution:
    def maxIncreasingSubarrays(self, nums: List[int]) -> int:
        n = len(nums)
        cnt, precnt, ans = 1, 0, 0
        for i in range(1, n):
            if nums[i] > nums[i - 1]:
                cnt += 1
            else:
                precnt = cnt
                cnt = 1
            ans = max(ans, min(precnt, cnt))
            ans = max(ans, cnt // 2)
        return ans

###C

int maxIncreasingSubarrays(int* nums, int numsSize) {
    int cnt = 1, precnt = 0, ans = 0;
    for (int i = 1; i < numsSize; ++i) {
        if (nums[i] > nums[i - 1]) {
            ++cnt;
        } else {
            precnt = cnt;
            cnt = 1;
        }
        int min_val = precnt < cnt ? precnt : cnt;
        ans = ans > min_val ? ans : min_val;
        ans = ans > cnt / 2 ? ans : cnt / 2;
    }
    return ans;
}

###JavaScript

var maxIncreasingSubarrays = function(nums) {
    const n = nums.length;
    let cnt = 1, precnt = 0, ans = 0;
    for (let i = 1; i < n; ++i) {
        if (nums[i] > nums[i - 1]) {
            ++cnt;
        } else {
            precnt = cnt;
            cnt = 1;
        }
        ans = Math.max(ans, Math.min(precnt, cnt));
        ans = Math.max(ans, Math.floor(cnt / 2));
    }
    return ans;
};

###TypeScript

function maxIncreasingSubarrays(nums: number[]): number {
    const n = nums.length;
    let cnt = 1, precnt = 0, ans = 0;
    for (let i = 1; i < n; ++i) {
        if (nums[i] > nums[i - 1]) {
            ++cnt;
        } else {
            precnt = cnt;
            cnt = 1;
        }
        ans = Math.max(ans, Math.min(precnt, cnt));
        ans = Math.max(ans, Math.floor(cnt / 2));
    }
    return ans;
}

###Rust

impl Solution {
    pub fn max_increasing_subarrays(nums: Vec<i32>) -> i32 {
        let n = nums.len();
        let mut cnt = 1;
        let mut precnt = 0;
        let mut ans = 0;
        
        for i in 1..n {
            if nums[i] > nums[i - 1] {
                cnt += 1;
            } else {
                precnt = cnt;
                cnt = 1;
            }
            ans = ans.max(precnt.min(cnt));
            ans = ans.max(cnt / 2);
        }
        
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(n)$。

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

每日一题-检测相邻递增子数组 II🟡

给你一个由 n 个整数组成的数组 nums ,请你找出 k最大值,使得存在 两个 相邻 且长度为 k严格递增 子数组。具体来说,需要检查是否存在从下标 ab (a < b) 开始的 两个 子数组,并满足下述全部条件:

  • 这两个子数组 nums[a..a + k - 1]nums[b..b + k - 1] 都是 严格递增 的。
  • 这两个子数组必须是 相邻的,即 b = a + k

返回 k最大可能 值。

子数组 是数组中的一个连续 非空 的元素序列。

 

示例 1:

输入:nums = [2,5,7,8,9,2,3,4,3,1]

输出:3

解释:

  • 从下标 2 开始的子数组是 [7, 8, 9],它是严格递增的。
  • 从下标 5 开始的子数组是 [2, 3, 4],它也是严格递增的。
  • 这两个子数组是相邻的,因此 3 是满足题目条件的 最大 k 值。

示例 2:

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

输出:2

解释:

  • 从下标 0 开始的子数组是 [1, 2],它是严格递增的。
  • 从下标 2 开始的子数组是 [3, 4],它也是严格递增的。
  • 这两个子数组是相邻的,因此 2 是满足题目条件的 最大 k 值。

 

提示:

  • 2 <= nums.length <= 2 * 105
  • -109 <= nums[i] <= 109

枚举

解法:枚举

这种一前一后两个子数组的题,一般考虑枚举分界点(比如第二个子数组的开头)。

维护 $f(i)$ 表示以第 $i$ 个元素为结尾,最长的单调递增子数组是多长;$g(i)$ 表示以第 $i$ 个元素为开头,最长的单调递增子数组是多长。那么我们枚举第二个子数组的开头 $b$,说明第一个子数组的结尾就是 $(b - 1)$,答案就是 $\max(\min(f(b - 1), g(b)))$。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###cpp

class Solution {
public:
    int maxIncreasingSubarrays(vector<int>& nums) {
        int n = nums.size();
        int f[n + 2], g[n + 2];
        f[0] = 0; g[n + 1] = 0;
        // 计算以第 i 个元素为结尾,最长的单调递增子数组是多长
        for (int i = 1; i <= n; i++) {
            if (i > 1 && nums[i - 2] < nums[i - 1]) f[i] = f[i - 1] + 1;
            else f[i] = 1;
        }
        // 计算以第 i 个元素为开头,最长的单调递增子数组是多长
        for (int i = n; i > 0; i--) {
            if (i < n && nums[i - 1] < nums[i]) g[i] = g[i + 1] + 1;
            else g[i] = 1;
        }
        int ans = 0;
        // 枚举第二个子数组的开头
        for (int i = 1; i <= n; i++) ans = max(ans, min(f[i - 1], g[i]));
        return ans;
    }
};
❌