题目地址:
https://www.lintcode.com/problem/delete-digits/description
给定一个字符串形式的数字 s s s,再给定一个数 k k k,要求在字符串中删去 k k k个字符,使得剩余字符组成的数字达到最小。返回那个最小的数,以字符串的形式返回即可。如果删除 k k k个数后得到了非零数但产生了首位的 0 0 0,则视为产生了去掉首位 0 0 0后的数。例如对于 101 101 101,如果允许删除 1 1 1个数,选择删除第一个 1 1 1,则得到的数是 01 = 1 01=1 01=1,开头的零可以直接舍去。
思路是贪心法。
我们先考虑只允许删掉一个数的情况。那么从左向右遍历字符串,同时每次都去考虑当前字符
s
[
i
]
s[i]
s[i]删掉是否能得到最小的数。很显然,当第一次遇到
s
[
i
]
>
s
[
i
+
1
]
s[i]>s[i+1]
s[i]>s[i+1]时,删掉
s
[
i
]
s[i]
s[i]会得到最小的数:
首先如果删掉
i
i
i之前的字符,由于
s
[
0
]
≤
s
[
1
]
≤
.
.
.
≤
s
[
i
−
1
]
s[0]\le s[1]\le ...\le s[i-1]
s[0]≤s[1]≤...≤s[i−1],所以删掉其中任何的一个字符,都会产生相等或者更大的数;而如果删除
i
i
i之后的字符,那么删除后得到的数一定是比删
s
[
i
]
s[i]
s[i]要大的,因为两个数比较第
i
i
i高位的话,
s
[
i
]
>
s
[
i
+
1
]
s[i]>s[i+1]
s[i]>s[i+1]。这就证明了那个结论。而如果整个字符串都满足
s
[
0
]
≤
s
[
1
]
≤
.
.
.
≤
s
[
m
]
s[0]\le s[1]\le ...\le s[m]
s[0]≤s[1]≤...≤s[m],那就删掉
s
[
m
]
s[m]
s[m]可以得到最小的数。
对于要删掉 k k k个数的情况,我们分成 k k k步来做,每次删除 1 1 1个即可。那么为什么每一步做贪心选择,最后结果就是最优的呢?
原因在于一个很显然的结论:如果 x x x和 y y y有相同的位数,且 x ≤ y x\le y x≤y,那么对两个数做删掉 k k k位的操作 f f f,得到的最小数 f x ( x ) f_x(x) fx(x)和 f y ( y ) f_y(y) fy(y),一定也是 f x ( x ) ≤ f y ( y ) f_x(x)\le f_y(y) fx(x)≤fy(y)。证明如下:假设删除 x x x得到最小数的操作为 f x f_x fx,如果对于 y y y采取了删除若干位的操作 g g g,那么我们可以对 x x x的对应位置也删掉,所以得 f ( x ) ≤ g ( x ) ≤ g ( y ) f(x)\le g(x)\le g(y) f(x)≤g(x)≤g(y),这对任意的 g g g都成立,当然对最优的那个 g = f y g=f_y g=fy也成立。
由这个结论我们就可以证明贪心法的正确性了:
反证法。如果存在一种删法,它与贪心法在某一步的选择上不同,而得到了更小的数,那么考虑那一步不同的选择,那次不同的选择后产生的两个数,比如为 u u u和 v v v,其中 u u u是贪心选择的, v v v不是,所以有 u ≤ v u\le v u≤v。由上面的引理,继续删除若干数字时 f u ( u ) ≤ f v ( v ) f_u(u)\le f_v(v) fu(u)≤fv(v),这就与得到了更小的数矛盾。证毕。
在实现方面,这使得我们想到了用单调栈来做,维护一个单调非降的栈,每次遇到相等或更大的新数则直接push进栈,否则弹掉栈顶直到栈顶小于等于新数。同时,每次删除的时候还要记录一下已经删过多少个数字,以便删满 k k k个要及时退出。
public class Solution {
/**
* @param A: A positive integer which has N digits, A is a string
* @param k: Remove k digits
* @return: A string
*/
public String DeleteDigits(String A, int k) {
// write your code here
// 直接用一个StringBuilder来当做栈使用
StringBuilder sb = new StringBuilder();
int count = k;
for (int i = 0; i < A.length(); i++) {
// 如果栈非空并且栈顶数字大于新来的数,则删掉栈顶,维护栈的单调非降性质,
// 同时记录还要删除的数字个数减少1
while (sb.length() > 0 && sb.charAt(sb.length() - 1) > A.charAt(i) && count > 0) {
sb.setLength(sb.length() - 1);
count--;
}
// 栈的单调性维护后直接加入新数
sb.append(A.charAt(i));
}
// 如果没删够k个,则直接删掉末尾的数
sb.setLength(A.length() - k);
// 删掉开始的0
int i = 0;
while (i < sb.length() && sb.charAt(i) == '0') {
i++;
}
// 如果全是0,说明删掉k个数后得到了0,则返回"0";否则返回去掉开头0之后的数
return sb.length() == 0 ? "0" : sb.substring(i);
}
}
时空复杂度 O ( n ) O(n) O(n), n n n为 A A A的长度。