题解: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(1)\)。
时间复杂度 \(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(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);
}
};