普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月10日LeetCode 每日一题题解

每日一题-统计强大整数的数目🔴

2025年4月10日 00:00

给你三个整数 start ,finish 和 limit 。同时给你一个下标从 0 开始的字符串 s ,表示一个  整数。

如果一个  整数 x 末尾部分是 s (换句话说,s 是 x 的 后缀),且 x 中的每个数位至多是 limit ,那么我们称 x 是 强大的 。

请你返回区间 [start..finish] 内强大整数的 总数目 。

如果一个字符串 x 是 y 中某个下标开始(包括 0 ),到下标为 y.length - 1 结束的子字符串,那么我们称 x 是 y 的一个后缀。比方说,25 是 5125 的一个后缀,但不是 512 的后缀。

 

示例 1:

输入:start = 1, finish = 6000, limit = 4, s = "124"
输出:5
解释:区间 [1..6000] 内的强大数字为 124 ,1124 ,2124 ,3124 和 4124 。这些整数的各个数位都 <= 4 且 "124" 是它们的后缀。注意 5124 不是强大整数,因为第一个数位 5 大于 4 。
这个区间内总共只有这 5 个强大整数。

示例 2:

输入:start = 15, finish = 215, limit = 6, s = "10"
输出:2
解释:区间 [15..215] 内的强大整数为 110 和 210 。这些整数的各个数位都 <= 6 且 "10" 是它们的后缀。
这个区间总共只有这 2 个强大整数。

示例 3:

输入:start = 1000, finish = 2000, limit = 4, s = "3000"
输出:0
解释:区间 [1000..2000] 内的整数都小于 3000 ,所以 "3000" 不可能是这个区间内任何整数的后缀。

 

提示:

  • 1 <= start <= finish <= 1015
  • 1 <= limit <= 9
  • 1 <= s.length <= floor(log10(finish)) + 1
  • s 数位中每个数字都小于等于 limit 。
  • s 不包含任何前导 0 。

进制转换

2024年1月12日 20:59

Problem: 2999. 统计强大整数的数目

[toc]

思路

可以将题目理解为:把 $s$ 看成一个数的后缀,在它的前面加上一个前缀,使得新的数在 $[start,finish]$ 之中,并且数的每一位数都要小于等于 $limit$。显然,加的这个前缀应当有个范围:$[low,upper]$。
每一位数都要小于等于 $limit$,可以想到这是一个 $limit+1$进制数(后面就叫它 $li$ 进制数)。
那所要求的答案便是,将 $low$ 和 $upper$ 看成是 $li$进制数之后,二者的差加 $1$(因为是闭区间)。

那接下来就是怎么求 $low$ 和 $upper$ 了。

求下界 $low$

先求 $low$ 。
注意:虽然都叫 $low$,但现在开始讨论的 $low$ 是带后缀的,结束之后去掉后缀得到前面说的那个 “前缀$low$”。后面 $upper$ 也是。
注意2:题目给的 $s$ 是字符串,但有的时候我就直接当整型啦~

下界 $low$ 要符合一些要求:

  1. 要在 $[start,finish]$ 内,也就是 $\ge start$
  2. 后缀要是 $s$,所以 $\ge s$
  3. 每一位上的数都要 $\le limit$

由 $1$ 和 $2$ 可以将 $low$ 初始值设置为 $max(start,s)$。

再看每一位的值。
假设现在的 $low$ 为 $\underline{a} \space \underline{b} \space \underline{c} \space \underline{{\color{Red} r} } \space \underline{x} \space \underline{y} \space \underline{z} $,某一位上的值是 $r$ 且 $r \gt limit$,那么比这 $low$ 大的(要找下界肯定不能往小的方向去考虑吧,往小的方向考虑说不定就比 $start$ 还小了呢。同样地,上界就应当往小的方向考虑)又要保证每一位数都 $\le limit$ 的最小的数是多少呢?那应该是 $ \underline{a} \space \underline{b} \space \underline{c+1} \space \underline{{\color{Red} 0} } \space \underline{0} \space \underline{0} \space \underline{0} $ 吧,也就是向前进位一位,同时将低位的值都变为 $0$。

因为要往前进位一位,而进位之后可能也会超过 $limit$,所以要从低位开始遍历。

还有一件事情,要提前做。
现在这个 $low$ 的后缀,如果比 $s$ 要大,那它的前缀部分也应当要加 $1$ 后缀部分全部变为 $0$,也就是和前面那样的操作。
假设现在 $low$ 为 $ \underline{low \underline{\space}pre} \space \underline{low\underline{\space}suf} $,其中 $ low\underline{\space}suf \gt s$,那么,为了找到后缀是 $s$ 的数,又不能往小了考虑,就要变大 $low$,让 $low$ 变成 $ \underline{low \underline{\space}pre+1} \space \underline{s} $。为了方便以及和前统一,也就将后缀每一位变为 $0$ ,这不影响。

现在得到了带后缀的下界 $low$,将后缀“去掉”,就得到了前面所说的“前缀范围”的那个 $low$了。

求上界 $upper$

再说 $upper$ .大思路和求 $low$ 差不多。

上界 $upper$ 要符合一些要求:

  1. 要在 $[start,finish]$ 内,也就是 $upper \le finish$
  2. 每一位上的数都要 $\le limit$

由 $1$ 可以将 $upper$ 初始值设置为 $finish$。同时,可以得到两个特判:当 $s \gt finish$ 的时候,可以直接 return 0 了,因为不会有在 $[start,finish]$ 的数;当 $s == finish$ 的时候,可以直接 return 1

再看每一位的值。
假设现在的 $upper$ 为 $\underline{a} \space \underline{b} \space \underline{c} \space \underline{{\color{Red} r} } \space \underline{x} \space \underline{y} \space \underline{z} $,某一位上的值是 $r$ 且 $r \gt limit$,那么比这 $upper$ 小的(为什么是小前面有说哦)又要保证每一位数都 $\lt limit$ 的最大的数是多少呢?那应该是 $ \underline{a} \space \underline{b} \space \underline{c} \space \underline{{\color{Red} limit} } \space \underline{limit} \space \underline{limit} \space \underline{limit} $ 吧,也就是从当前位开始往低位全部变为 $limit$。

因为要修改低位的值,所以选择从高位开始遍历。

提前要做的事情。
假设现在的 $upper$ 是 $ \underline{upper\underline{\space}pre} \space \underline{upper\underline{\space}suf} $,其中 $ upper\underline{\space}suf \lt s$,这样的 $upper$ 是大了的,要变成 $ \underline{upper\underline{\space}pre-1} \space \underline{upper\underline{\space}suf} $ 才行。

现在得到了带后缀的上界 $upper$,将后缀“去掉”,就得到了开始所说的“前缀范围”的那个 $upper$ 了。

Code

###Python3

class Solution:
    def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
        s_int = int(s)
        if s_int >= finish:  # s 本身就比 finish 大,那在 [start,finish] 里就没有强数了,但如果 s==finish,还能有一个
            return int(s_int == finish)

        def int_len(x: int) -> int:
            # 计算 整数 x 的长度
            ans = 0
            while x:
                ans += 1
                x //= 10
            return ans

        s_len = len(s)
        sk = 10 ** s_len  # 经常用到

        low = max(s_int, start)
        if s_int < start % sk:
            # 向前进位,后面全减少为0
            low += sk - start % sk
        # 每一位都要比 limit 小或等于,哪一位大了往前进位,再将这一位及其往后的变为 0
        for k in range(s_len, int_len(low)):
            kk = pow(10, k)
            r = low // kk % 10  # 当前这一位的值
            if r > limit:
                low += (10 - r) * kk  # 当前位往前进 1 # low += kk * 10
                low = low // kk * kk  # 将后面全部变成 0
        low //= sk

        upper = finish - (10 ** s_len if s_int > finish % sk else 0)
        # 每一位都要比 limit 小或等于,哪一位大了就把这一位及其往后都变成 limit
        flag = False
        for k in range(int_len(upper), s_len - 1, -1):
            kk = pow(10, k)
            r = upper // kk % 10  # 当前这一位的值
            if flag or r > limit:  # 当前这一位变为 limit
                upper -= (r - limit) * kk
                flag = True
        upper //= sk

        def f(x: int) -> int:
            li = limit + 1
            # 将 li进制 数转为十进制数
            ans = 0
            k = 1
            while x:
                ans += x % 10 * k
                k *= li
                x //= 10
            return ans

        return f(upper) - f(low) + 1

复杂度

时间复杂度:$O( \log_{10}{start} + \log_{10}{finish})$ ,时间复杂度与 $start$ 和 $finish$ 的数位长度有关。
空间复杂度:$O(1)$,只有一些额外变量。

面向解答错误做题😪

7次解答错误的提交记录

【模板】上下界数位 DP(Python/Java/C++/Go)

作者 endlesscheng
2024年1月7日 21:02

v1.0 视频讲解

v2.0 视频讲解

定义 $\textit{dfs}(i,\textit{limitLow},\textit{limitHigh})$ 表示构造第 $i$ 位及其之后数位的合法方案数,其余参数的含义为:

  • $\textit{limitHigh}$ 表示当前是否受到了 $\textit{finish}$ 的约束(我们要构造的数字不能超过 $\textit{finish}$)。若为真,则第 $i$ 位填入的数字至多为 $\textit{finish}[i]$,否则至多为 $9$,这个数记作 $\textit{hi}$。如果在受到约束的情况下填了 $\textit{finish}[i]$,那么后续填入的数字仍会受到 $\textit{finish}$ 的约束。例如 $\textit{finish}=123$,那么 $i=0$ 填的是 $1$ 的话,$i=1$ 的这一位至多填 $2$。
  • $\textit{limitLow}$ 表示当前是否受到了 $\textit{start}$ 的约束(我们要构造的数字不能低于 $\textit{start}$)。若为真,则第 $i$ 位填入的数字至少为 $\textit{start}[i]$,否则至少为 $0$,这个数记作 $\textit{lo}$。如果在受到约束的情况下填了 $\textit{start}[i]$,那么后续填入的数字仍会受到 $\textit{start}$ 的约束。

枚举第 $i$ 位填什么数字。

如果 $i< n - |s|$,那么可以填 $[\textit{lo}, \min(\textit{hi}, \textit{limit})]$ 内的数,否则只能填 $s[i-(n-|s|)]$。这里 $|s|$ 表示 $s$ 的长度。

为什么不能把 $\textit{hi}$ 置为 $\min(\textit{hi}, \textit{limit})$?请看 视频 中举的反例。

递归终点:$\textit{dfs}(n,,)=1$,表示成功构造出一个合法数字。

递归入口:$\textit{dfs}(0, \texttt{true}, \texttt{true})$,表示:

  • 从最高位开始枚举。
  • 一开始要受到 $\textit{start}$ 和 $\textit{finish}$ 的约束(否则就可以随意填了,这肯定不行)。

答疑

:记忆化三个状态有点麻烦,能不能只记忆化 $i$ 这个状态?

:是可以的。比如 $\textit{finish}=234$,第一位填 $2$,第二位填 $3$,后面无论怎么递归,都不会再次递归到第一位填 $2$,第二位填 $3$ 的情况,所以不需要记录。对于 $\textit{start}$ 也同理。

根据这个例子,我们可以只记录不受到 $\textit{limitLow}$ 或 $\textit{limitHigh}$ 约束时的状态 $i$。相当于记忆化的是 $(i,\texttt{false},\texttt{false})$ 这个状态,因为其它状态只会递归访问一次。

###py

class Solution:
    def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
        high = list(map(int, str(finish)))  # 避免在 dfs 中频繁调用 int()
        n = len(high)
        low = list(map(int, str(start).zfill(n)))  # 补前导零,和 high 对齐
        diff = n - len(s)

        @cache
        def dfs(i: int, limit_low: bool, limit_high: bool) -> int:
            if i == n:
                return 1

            # 第 i 个数位可以从 lo 枚举到 hi
            # 如果对数位还有其它约束,应当只在下面的 for 循环做限制,不应修改 lo 或 hi
            lo = low[i] if limit_low else 0
            hi = high[i] if limit_high else 9

            res = 0
            if i < diff:  # 枚举这个数位填什么
                for d in range(lo, min(hi, limit) + 1):
                    res += dfs(i + 1, limit_low and d == lo, limit_high and d == hi)
            else:  # 这个数位只能填 s[i-diff]
                x = int(s[i - diff])
                if lo <= x <= min(hi, limit):
                    res = dfs(i + 1, limit_low and x == lo, limit_high and x == hi)
            return res

        return dfs(0, True, True)

###java

class Solution {
    public long numberOfPowerfulInt(long start, long finish, int limit, String s) {
        String low = Long.toString(start);
        String high = Long.toString(finish);
        int n = high.length();
        low = "0".repeat(n - low.length()) + low; // 补前导零,和 high 对齐
        long[] memo = new long[n];
        Arrays.fill(memo, -1);
        return dfs(0, true, true, low.toCharArray(), high.toCharArray(), limit, s.toCharArray(), memo);
    }

    private long dfs(int i, boolean limitLow, boolean limitHigh, char[] low, char[] high, int limit, char[] s, long[] memo) {
        if (i == high.length) {
            return 1;
        }

        if (!limitLow && !limitHigh && memo[i] != -1) {
            return memo[i]; // 之前计算过
        }

        // 第 i 个数位可以从 lo 枚举到 hi
        // 如果对数位还有其它约束,应当只在下面的 for 循环做限制,不应修改 lo 或 hi
        int lo = limitLow ? low[i] - '0' : 0;
        int hi = limitHigh ? high[i] - '0' : 9;

        long res = 0;
        if (i < high.length - s.length) { // 枚举这个数位填什么
            for (int d = lo; d <= Math.min(hi, limit); d++) {
                res += dfs(i + 1, limitLow && d == lo, limitHigh && d == hi, low, high, limit, s, memo);
            }
        } else { // 这个数位只能填 s[i-diff]
            int x = s[i - (high.length - s.length)] - '0';
            if (lo <= x && x <= Math.min(hi, limit)) {
                res = dfs(i + 1, limitLow && x == lo, limitHigh && x == hi, low, high, limit, s, memo);
            }
        }

        if (!limitLow && !limitHigh) {
            memo[i] = res; // 记忆化 (i,false,false)
        }
        return res;
    }
}

###cpp

class Solution {
public:
    long long numberOfPowerfulInt(long long start, long long finish, int limit, string s) {
        string low = to_string(start);
        string high = to_string(finish);
        int n = high.size();
        low = string(n - low.size(), '0') + low; // 补前导零,和 high 对齐
        int diff = n - s.size();

        vector<long long> memo(n, -1);
        auto dfs = [&](this auto&& dfs, int i, bool limit_low, bool limit_high) -> long long {
            if (i == low.size()) {
                return 1;
            }

            if (!limit_low && !limit_high && memo[i] != -1) {
                return memo[i]; // 之前计算过
            }

            // 第 i 个数位可以从 lo 枚举到 hi
            // 如果对数位还有其它约束,应当只在下面的 for 循环做限制,不应修改 lo 或 hi
            int lo = limit_low ? low[i] - '0' : 0;
            int hi = limit_high ? high[i] - '0' : 9;

            long long res = 0;
            if (i < diff) { // 枚举这个数位填什么
                for (int d = lo; d <= min(hi, limit); d++) {
                    res += dfs(i + 1, limit_low && d == lo, limit_high && d == hi);
                }
            } else { // 这个数位只能填 s[i-diff]
                int x = s[i - diff] - '0';
                if (lo <= x && x <= min(hi, limit)) {
                    res = dfs(i + 1, limit_low && x == lo, limit_high && x == hi);
                }
            }

            if (!limit_low && !limit_high) {
                memo[i] = res; // 记忆化 (i,false,false)
            }
            return res;
        };
        return dfs(0, true, true);
    }
};

###go

func numberOfPowerfulInt(start, finish int64, limit int, s string) int64 {
low := strconv.FormatInt(start, 10)
high := strconv.FormatInt(finish, 10)
n := len(high)
low = strings.Repeat("0", n-len(low)) + low // 补前导零,和 high 对齐
diff := n - len(s)

memo := make([]int64, n)
for i := range memo {
memo[i] = -1
}
var dfs func(int, bool, bool) int64
dfs = func(i int, limitLow, limitHigh bool) (res int64) {
if i == n {
return 1
}

if !limitLow && !limitHigh {
p := &memo[i]
if *p >= 0 {
return *p
}
defer func() { *p = res }()
}

// 第 i 个数位可以从 lo 枚举到 hi
// 如果对数位还有其它约束,应当只在下面的 for 循环做限制,不应修改 lo 或 hi
lo := 0
if limitLow {
lo = int(low[i] - '0')
}
hi := 9
if limitHigh {
hi = int(high[i] - '0')
}

if i < diff { // 枚举这个数位填什么
for d := lo; d <= min(hi, limit); d++ {
res += dfs(i+1, limitLow && d == lo, limitHigh && d == hi)
}
} else { // 这个数位只能填 s[i-diff]
x := int(s[i-diff] - '0')
if lo <= x && x <= min(hi, limit) {
res += dfs(i+1, limitLow && x == lo, limitHigh && x == hi)
}
}
return
}
return dfs(0, true, true)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nD)$,其中 $n=\mathcal{O}(\log \textit{finish})$,$D=10$。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(n)$,单个状态的计算时间为 $\mathcal{O}(D)$,所以动态规划的时间复杂度为 $\mathcal{O}(nD)$。
  • 空间复杂度:$\mathcal{O}(n)$。即状态个数。

分类题单

如何科学刷题?

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

不用数位DP的计数方法

作者 FreeYourMind
2024年1月7日 00:59

###Python3

class Solution:
    def numberOfPowerfulInt(self, start: int, finish: int, limit: int, s: str) -> int:
        x, m = int(s), len(s)

        def ceil(y: int):
            sy = str(y)
            pre, suf = int("0" + sy[:-m]), int(sy[-m:])
            return pre + int(suf >= x)

        def count(y: int) -> int:
            n, res = len(str(y)), 0
            for i, d in enumerate(map(int, str(y))):
                if limit < d:
                    return res + pow(limit + 1, n - i)
                res += d * pow(limit + 1, n - i - 1)
            return res

        b, a = ceil(finish), ceil(start - 1)
        return count(b) - count(a)
昨天 — 2025年4月9日LeetCode 每日一题题解

使数组的值全部为 K 的最少操作次数

2025年4月1日 11:03

方法一:哈希表

思路与算法

根据题意,若当前数组的最大值为 $x$,次大值(若有的话)为 $y$,那么我们可以选择一个 $h(y \le h \lt x)$,令数组中所有等于 $x$ 的数字都变成 $h$。

因此,若要使用最小的操作次数把整个数组中的数字都变成 $k$,那么:

  • 若数组存在比 $k$ 小的数字,则无解;
  • 否则,统计数组中所有大于 $k$ 的数字种类个数,该个数即为操作次数;

我们用一个哈希表去统计数组中大于 $k$ 的数字。在遍历数组的过程中,若遇到比 $k$ 小的则直接返回 $-1$。

代码

###C++

class Solution {
public:
    int minOperations(vector<int>& nums, int k) {
        unordered_set<int> st;
        for (int x : nums) {
            if (x < k) {
                return -1;
            } else if (x > k) {
                st.insert(x);
            }
        }
        return st.size();
    }
};

###Python

class Solution:
    def minOperations(self, nums: List[int], k: int) -> int:
        st = set()
        for x in nums:
            if x < k:
                return -1
            elif x > k:
                st.add(x)
        return len(st)

###Rust

use std::collections::HashSet;
impl Solution {
    pub fn min_operations(nums: Vec<i32>, k: i32) -> i32 {
        let mut st = HashSet::new();
        for x in nums {
            if x < k {
                return -1
            } else if x > k {
                st.insert(x);
            }
        }
        st.len() as i32
    }
}

###Java

class Solution {
    public int minOperations(int[] nums, int k) {
        Set<Integer> st = new HashSet<>();
        for (int x : nums) {
            if (x < k) {
                return -1;
            } else if (x > k) {
                st.add(x);
            }
        }
        return st.size();
    }
}

###C#

public class Solution {
    public int MinOperations(int[] nums, int k) {
        HashSet<int> st = new HashSet<int>();
        foreach (int x in nums) {
            if (x < k) {
                return -1;
            } else if (x > k) {
                st.Add(x);
            }
        }
        return st.Count;
    }
}

###Go

func minOperations(nums []int, k int) int {
    st := make(map[int]bool)
    for _, x := range nums {
        if x < k {
            return -1
        } else if x > k {
            st[x] = true
        }
    }
    return len(st)
}

###C

typedef struct {
    int key;
    UT_hash_handle hh;
} HashItem;

HashItem *hashFindItem(HashItem **obj, int key) {
    HashItem *pEntry = NULL;
    HASH_FIND_INT(*obj, &key, pEntry);
    return pEntry;
}

bool hashAddItem(HashItem **obj, int key) {
    if (hashFindItem(obj, key)) {
        return false;
    }
    HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
    pEntry->key = key;
    HASH_ADD_INT(*obj, key, pEntry);
    return true;
}

void hashFree(HashItem **obj) {
    HashItem *curr = NULL, *tmp = NULL;
    HASH_ITER(hh, *obj, curr, tmp) {
        HASH_DEL(*obj, curr);  
        free(curr);
    }
}
int minOperations(int* nums, int numsSize, int k) {
    HashItem *st = NULL;
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] < k) {
            return -1;
        } else if (nums[i] > k) {
            hashAddItem(&st, nums[i]);
        }
    }
    int ret = HASH_COUNT(st);
    hashFree(&st);
    return ret;
}

###JavaScript

var minOperations = function(nums, k) {
    const st = new Set();
    for (const x of nums) {
        if (x < k) {
            return -1;
        } else if (x > k) {
            st.add(x);
        }
    }
    return st.size;
};

###TypeScript

function minOperations(nums: number[], k: number): number {
    const st = new Set<number>();
    for (const x of nums) {
        if (x < k) {
            return -1;
        } else if (x > k) {
            st.add(x);
        }
    }
    return st.size;
};

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。我们仅需遍历一次 $\textit{nums}$,并且向哈希表中添加元素的时间复杂度为 $O(1)$,因此总体时间复杂度为 $O(n)$。

  • 空间复杂度:$O(n)$。使用哈希表的空间复杂度为 $O(n)$。

[Python3/Java/C++/Go/TypeScript] 一题一解:哈希表(清晰题解)

作者 lcbin
2025年4月9日 06:36

方法一:哈希表

根据题目描述,我们每次可以选择当前数组中的次大值作为合法整数 $h$,将所有大于 $h$ 的数都变为 $h$,这样可以使得操作次数最少。另外,由于操作会使得数字变小,因此,如果当前数组中存在小于 $k$ 的数,那么我们就无法将所有数都变为 $k$,直接返回 -1 即可。

我们遍历数组 $\textit{nums}$,对于当前的数 $x$,如果 $x < k$,直接返回 -1;否则,我们将 $x$ 加入哈希表中,并且更新当前数组中的最小值 $\textit{mi}$。最后,我们返回哈希表的大小减去 1(如果 $\textit{mi} = k$,则需要减去 1)。

###python

class Solution:
    def minOperations(self, nums: List[int], k: int) -> int:
        s = set()
        mi = inf
        for x in nums:
            if x < k:
                return -1
            mi = min(mi, x)
            s.add(x)
        return len(s) - int(k == mi)

###java

class Solution {
    public int minOperations(int[] nums, int k) {
        Set<Integer> s = new HashSet<>();
        int mi = 1 << 30;
        for (int x : nums) {
            if (x < k) {
                return -1;
            }
            mi = Math.min(mi, x);
            s.add(x);
        }
        return s.size() - (mi == k ? 1 : 0);
    }
}

###cpp

class Solution {
public:
    int minOperations(vector<int>& nums, int k) {
        unordered_set<int> s;
        int mi = INT_MAX;
        for (int x : nums) {
            if (x < k) {
                return -1;
            }
            mi = min(mi, x);
            s.insert(x);
        }
        return s.size() - (mi == k);
    }
};

###go

func minOperations(nums []int, k int) int {
mi := 1 << 30
s := map[int]bool{}
for _, x := range nums {
if x < k {
return -1
}
s[x] = true
mi = min(mi, x)
}
if mi == k {
return len(s) - 1
}
return len(s)
}

###ts

function minOperations(nums: number[], k: number): number {
    const s = new Set<number>();
    let mi = Infinity;
    for (const x of nums) {
        if (x < k) {
            return -1;
        }
        s.add(x);
        mi = Math.min(mi, x);
    }
    return s.size - (mi === k ? 1 : 0);
}

###rust

impl Solution {
    pub fn min_operations(nums: Vec<i32>, k: i32) -> i32 {
        use std::collections::HashSet;

        let mut s = HashSet::new();
        let mut mi = i32::MAX;

        for &x in &nums {
            if x < k {
                return -1;
            }
            s.insert(x);
            mi = mi.min(x);
        }

        (s.len() as i32) - if mi == k { 1 } else { 0 }
    }
}

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


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

每日一题-使数组的值全部为 K 的最少操作次数🟢

2025年4月9日 00:00

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

如果一个数组中所有 严格大于 h 的整数值都 相等 ,那么我们称整数 h 是 合法的 。

比方说,如果 nums = [10, 8, 10, 8] ,那么 h = 9 是一个 合法 整数,因为所有满足 nums[i] > 9 的数都等于 10 ,但是 5 不是 合法 整数。

你可以对 nums 执行以下操作:

  • 选择一个整数 h ,它对于 当前 nums 中的值是合法的。
  • 对于每个下标 i ,如果它满足 nums[i] > h ,那么将 nums[i] 变为 h 。

你的目标是将 nums 中的所有元素都变为 k ,请你返回 最少 操作次数。如果无法将所有元素都变 k ,那么返回 -1 。

 

示例 1:

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

输出:2

解释:

依次选择合法整数 4 和 2 ,将数组全部变为 2 。

示例 2:

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

输出:-1

解释:

没法将所有值变为 2 。

示例 3:

输入:nums = [9,7,5,3], k = 1

输出:4

解释:

依次选择合法整数 7 ,5 ,3 和 1 ,将数组全部变为 1 。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 100
  • 1 <= k <= 100

本质是计算不同元素个数(Python/Java/C++/Go/JS/Rust)

作者 endlesscheng
2024年12月8日 09:11

思考框架

  1. 理解操作在做什么。
  2. 把 $\textit{nums}$ 中的数都变成一样的,能变成哪些数?
  3. 如何最小化操作次数?

操作在做什么

题目说「选择一个整数 $h$」,哪些 $h$ 是可以选的?

根据「合法」的定义,$h$ 不能低于 $\textit{nums}$ 的次大值。比如 $\textit{nums}=[5,2,5,4,5]$,$h$ 不能小于次大值 $4$,否则大于 $h$ 的数不都相等。

所以操作只能改变大于次大值的数,也就是最大值

能变成哪些数

要让所有数都变成 $k$,前提条件是所有数都变成一样的。那么,能变成哪些数呢?

仍然以 $\textit{nums}=[5,2,5,4,5]$ 为例。选择 $h=4$,可以把最大值 $5$ 改成次大值 $4$。修改后 $\textit{nums}=[4,2,4,4,4]$,有更多的数都相同。并且修改后,原来的次大值 $4$ 就变成最大值了。下一次操作,我们就可以选择更小的 $h$,把更多的数都变成一样的。

选择 $h=2$,可以把最大值 $4$ 改成次大值 $2$。修改后 $\textit{nums}=[2,2,2,2,2]$,所有数都相同。

如果想继续改成比 $2$ 小的数,比如 $0$,选择 $h=0$ 即可。

所以,$\textit{nums}$ 中的数可以都变成任意 $\le \min(\textit{nums})$ 的数。

最小化操作次数

为了最小化操作次数,每次选 $h$ 为次大值是最优的。贪心地想,能一步到达次大值,没必要分好几步。

分类讨论:

  • 如果 $k > \min(nums)$,无法满足要求,返回 $-1$。
  • 如果 $k = \min(nums)$,操作次数为 $\textit{nums}$ 中的不同元素个数减一。比如 $[5,2,5,4,5]\to [4,2,4,4,4]\to [2,2,2,2,2]$,最大值 $5\to 4\to 2$,用了 $2$ 次操作。
  • 如果 $k < \min(nums)$,操作次数为 $\textit{nums}$ 中的不同元素个数。因为都变成 $\min(nums)$ 后,还需要再操作一次,才能都变成 $k$。

###py

class Solution:
    def minOperations(self, nums: List[int], k: int) -> int:
        mn = min(nums)
        if k > mn:
            return -1
        return len(set(nums)) - (k == mn)

###java

class Solution {
    public int minOperations(int[] nums, int k) {
        int min = Arrays.stream(nums).min().getAsInt();
        if (k > min) {
            return -1;
        }
        int distinctCount = (int) Arrays.stream(nums).distinct().count();
        return distinctCount - (k == min ? 1 : 0);
    }
}

###cpp

class Solution {
public:
    int minOperations(vector<int>& nums, int k) {
        int mn = ranges::min(nums);
        if (k > mn) {
            return -1;
        }
        unordered_set<int> st(nums.begin(), nums.end());
        return st.size() - (k == mn);
    }
};

###go

func minOperations(nums []int, k int) int {
    mn := slices.Min(nums)
    if k > mn {
        return -1
    }

    set := map[int]struct{}{}
    for _, x := range nums {
        set[x] = struct{}{}
    }
    if k == mn {
        return len(set) - 1
    }
    return len(set)
}

###js

var minOperations = function(nums, k) {
    const min = Math.min(...nums);
    if (k > min) {
        return -1;
    }
    return new Set(nums).size - (k === min ? 1 : 0);
};

###rust

use std::collections::HashSet;

impl Solution {
    pub fn min_operations(nums: Vec<i32>, k: i32) -> i32 {
        let min = *nums.iter().min().unwrap();
        if k > min {
            return -1;
        }
        let set = nums.into_iter().collect::<HashSet<_>>();
        set.len() as i32 - if k == min { 1 } else { 0 }
    }
}

复杂度分析

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

分类题单

如何科学刷题?

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

❌
❌