题解:LeetCode 402 移掉 k 位数字 & 洛谷 P1106 删数问题

LeetCode 402 移掉 k 位数字 & 洛谷 P1106 删数问题

题意

给出一个数字 \(x\),删除其中 \(k\) 位,问结果最小是多少。

\(\lg x\le5\times10^5\)

解法 1(构造法)

初始版

最后剩余的位数是 \(\lg x-k\)。由于数字长度相同时大小取决于字典序大小,那么就可以从前往后遍历第 \(i\) 位,尝试构造这个字符串。

每一位可以选择 \(0\sim 9\)(前导 \(0\) 最后删去),那么我们从小到大遍历 \(0\)\(9\),这样就可以使得第 \(i\) 位尽量小。

如何判断数 \(x\) 的合法性呢?

我们可以记录上一次选取数字的位置 \(p\),那么显然在以 \(p+1\) 为开头的后缀中必然要存在 \(x\)

当然,我们要保证选了 \(x\) 之后剩余的数字够填满尚未填的空格。

时间复杂度

\(n=\lg x\)

  • 遍历数字的每一位,\(O(n)\)
    • 枚举每一位的数字,\(O(1)\)
      • 判断是否可行,可以用 find\(O(n)\)

时间复杂度 \(O(n^2)\),理论上不能通过。

代码
class Solution {
    std::vector<size_t> id[10]; // id[num][x] 表示以 x 为开头的后缀中最靠前的 num 的位置
    void init(const std::string &num) {
        for (int i = 0; i < 10; ++i) id[i].resize(num.size() + 1), id[i].back() = std::string::npos;
        for (int i = num.size() - 1; ~i; --i) {
            for (int j = 0; j < 10; ++j) {
                if (num[i] == j + '0') id[j][i] = i;
                else id[j][i] = id[j][i + 1];
            }
        }
    }
public:
  string removeKdigits(string num, int k) {
    init(num);
    std::string res = "";
    size_t pos = -1, tmp;
    bool zero = true;
    for (int i = 0; i < num.size() - k; ++i) { // 遍历生成数字的每一位
      for (int x = 0; x < 10; ++x) {
        tmp = id[x][pos + 1];
        if (tmp != std::string::npos && num.size() - tmp >= num.size() - k - i) { // 可以选择 x
            pos = tmp;
            zero &= x == 0;
            if (!zero) res.push_back(x + '0');
            break;
        }
      }
    }
    if (res.empty()) res = "0";
    return res;
  }
};

升级版

考虑对 find() 的部分进行优化。

容易发现,我们每次查询都是查询一个后缀中最靠前的某个数字的位置

以空间换时间,我们就可以从后往前预处理这个位置,把判定的复杂度降为 \(O(1)\)

时间复杂度
  • 遍历数字的每一位,\(O(n)\)
    • 枚举每一位的数字,\(O(1)\)
      • 判断是否可行,\(O(1)\)

时间复杂度 \(O(n)\),可以通过。

代码
class Solution {
    std::vector<size_t> id[10]; // id[num][x] 表示以 x 为开头的后缀中最靠前的 num 的位置
    void init(const std::string &num) { // 预处理
        for (int i = 0; i < 10; ++i) id[i].resize(num.size() + 1), id[i].back() = std::string::npos;
        for (int i = num.size() - 1; ~i; --i) {
            for (int j = 0; j < 10; ++j) {
                if (num[i] == j + '0') id[j][i] = i;
                else id[j][i] = id[j][i + 1];
            }
        }
    }
public:
  string removeKdigits(string num, int k) {
    init(num);
    std::string res = "";
    size_t pos = -1, tmp;
    bool zero = true;
    for (int i = 0; i < num.size() - k; ++i) { // 遍历生成数字的每一位
      for (int x = 0; x < 10; ++x) {
        tmp = id[x][pos + 1];
        if (tmp != std::string::npos && num.size() - tmp >= num.size() - k - i) { // 可以选择 x
            pos = tmp;
            zero &= x == 0;
            if (!zero) res.push_back(x + '0');
            break;
        }
      }
    }
    if (res.empty()) res = "0";
    return res;
  }
};

解法 2(删除法)

力扣官解 上看到的,十分巧妙。

引理:给定一个数字(字符串)\(S\),那么这一次删除数字的策略是:从前往后找到第一个位置 \(i\) 使得 \(S_i<S_{i-1}\),删除 \(S_{i-1}\);否则说明序列单调不降,删除最后一个数字。

  • 如果保留 \(S_{i-1}\),最后数字必然为 \(\text{xxxx}S_{i-1}\text{xxx}\)
  • 如果删除 \(S_{i-1}\),最后数字必然为 \(\text{xxxx}S_i\text{xxx}\)

显然删除 \(S_{i-1}\)​ 更好。

暴力做复杂度会达到 \(O(nk)\),考虑进行优化。


使用一个单调栈,然后从前往后扫,每个数把栈顶上的数弹出。

时间复杂度

每个数字只进出栈一次,\(O(n)\)​,可以通过。

代码

class Solution {
    std::deque<int> q;
public:
    string removeKdigits(string num, int k) {
        for (int i = 0; i < num.size(); ++i) {
            while (q.size() && num[i] - '0' < q.back() && k) q.pop_back(), --k;
            q.push_back(num[i] - '0');
        }
        while (k) q.pop_back(), --k;
        bool zero = true;
        std::string res;
        while (q.size()) {
            int x = q.front(); q.pop_front();
            zero &= x == 0;
            if (!zero) res.push_back(x + '0');
        }
        if (res.empty()) res = "0";
        return std::move(res);
    }
};
posted @ 2025-06-22 10:34  OIer_wst  阅读(17)  评论(0)    收藏  举报