【Leetcode】1505. Minimum Possible Integer After at Most K Adjacent Swaps On Digits

题目地址:

https://leetcode.com/problems/minimum-possible-integer-after-at-most-k-adjacent-swaps-on-digits/

给定一个正整数 n n n,以字符串 s s s的形式给出,题目保证没有开头 0 0 0。给定非负整数 k k k,允许最多做 k k k次对换。问能得到的最小数是多少。这个数可以有开头 0 0 0

要使得数字更小,那么容易想到要先尽可能地将最小数移到最高位,然后次小数移到次高位,以此类推。算法如下:
1、开 10 10 10个队列先将所有的 0 ∼ 9 0\sim 9 09从左到右每个数的位置存下来,以供后面取出;
2、然后从左到右扫描 s s s,对于每个位置,我们都尽可能地把后面最小的数字挪过来,如果有 0 0 0则挪 0 0 0(在剩余对换次数够的情况下,不够就算了),有 1 1 1则挪 1 1 1,以此类推。基于贪心的思想,我们希望挪的时候耗费尽可能少的次数,那么显然应该挪队头的那个下标。挪好了之后,发生对换的数都整体向后移动一位。容易看出整体向后移动一位的数形成字符串中的一个子串,如果用一个数组存储每个位置的数的向右的“偏移量”的话,这个操作相当于将某个区间集体加 1 1 1,这可以用差分数组来优化,在差分数组里对应着两次单点修改。但是我们也需要频繁查询队列 i i i的队头的下标的数所在的实际位置,这需要对差分数组的前缀求和,容易想到用树状数组维护差分数组,而树状数组的单点修改和查询前缀和的时间复杂度都是 O ( log ⁡ n ) O(\log n) O(logn)
3、在对换次数用尽之后返回答案即可。

关于贪心选择的正确性的问题,亦不难证明:
如果某次挪某个数的时候(比如挪 0 0 0),不去挪那个最靠左的 0 0 0,而非要去挪后面的某个 0 0 0(这其实隐含着后面的某个 0 0 0是能挪到最前面的,否则挪后面的 0 0 0就没有意义了),那么挪完之后我们考虑挪到的位置往后的后缀,相比于贪心选择,我们的剩余对换次数边少了,因为我们相当于把后面的 0 0 0向前挪了几位,如果对 s s s的长度做归纳,由归纳假设,贪心选择可以获得最优解,而非贪心选择面对的情况是对换次数变少,那么显然它能得到的最优解不会比贪心选择可以获得的最优解更优,从而证明了结论(本质上是说,一个大集合的最优解一定优于其子集的最优解)。

代码如下:

import java.util.ArrayDeque;
import java.util.Queue;

public class Solution {
    
    // 开一个树状数组类,维护每个位置的字符的向右的偏移量
    class BinaryIndexTree {
        
        private int size;
        private int[] tr;
        
        public BinaryIndexTree(int size) {
            this.size = size;
            tr = new int[size + 1];
        }
        
        public void add(int idx, int v) {
            while (idx <= size) {
                tr[idx] += v;
                idx += lowbit(idx);
            }
        }
        
        public int sum(int idx) {
            int res = 0;
            while (idx > 0) {
                res += tr[idx];
                idx -= lowbit(idx);
            }
            
            return res;
        }
        
        private int lowbit(int x) {
            return x & -x;
        }
    }
    
    public String minInteger(String s, int k) {
        int n = s.length();
        // 树状数组从1开始,所以字符串下标也要从1开始比较方便
        s = " " + s;
        // 先开10个队列,然后将每个数各自出现在什么位置存进去
        // 当然这里可以稍微优化一点,只需开9个队列就行了,因为我们不会主动挪9
        Queue<Integer>[] q = (Queue<Integer>[]) new ArrayDeque[10];
        for (int i = 1; i <= n; i++) {
            int x = s.charAt(i) - '0';
            if (q[x] == null) {
                q[x] = new ArrayDeque<>();
            }
            
            q[x].add(i);
        }
        
        BinaryIndexTree tr = new BinaryIndexTree(n);
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < 10; j++) {
                if (q[j] != null && !q[j].isEmpty()) {
                	// pos是真实的位置,真实位置是原来的位置加上偏移量
                    int t = q[j].peek(), pos = t + tr.sum(t);
                    // 如果k还够让j挪到开头的话,按照贪心选择,就挪
                    if (pos - i <= k) {
                        k -= pos - i;
                        sb.append(j);
                        q[j].poll();
                        tr.add(1, 1);
                        tr.add(t, -1);
                        break;
                    }
                }
            }
        }
        
        return sb.toString();
    }
}

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值