【Lintcode】182. Delete Digits

题目地址:

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[i1],所以删掉其中任何的一个字符,都会产生相等或者更大的数;而如果删除 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 xy,那么对两个数做删掉 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 uv。由上面的引理,继续删除若干数字时 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的长度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值