题目地址:
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
0∼9从左到右每个数的位置存下来,以供后面取出;
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)。