题目地址:
https://leetcode.com/problems/rearrange-string-k-distance-apart/
给定一个字符串 s s s,其长度为 l l l,给定一个非负整数 k k k,问是否可以将其重排,使得相距 k k k范围内的两个字母都不相等(恰好相距 k k k是可以的,但再近的就不能相等了)。如果能,返回重排后的字符串;否则返回空串。题目保证只含小写英文字母。
思路是贪心。想象我们有 l l l个空位,将其分成 l / k l/k l/k个大块,每个大块长度为 l / k l/k l/k,如果后面还有空位没有分配,则它们在最后成为一个长度小于 l / k l/k l/k的大块。那我们不允许在同一个大块里存在相同的字符,所以我们的策略是,在每个大块里,依次填出现次数最多的 k k k个字符(如果到了最后一个大块了,那么这个大块的长度小于 k k k,则填写的字符数小于 k k k。这里选择出现最多的 k k k个字符,原因在于直觉上来说,”潜在地“它们占用的地方最多,需要最先被分配掉),填写的时候,要按照字典序填,这样保证块与块之间也不会产生相同字母距离小于 k k k的情况(这里的字典序实际上可以是任何一种序,只需要是 26 26 26个英文字母的全序关系即可,不一定要按照 a b c d abcd abcd的顺序)。填完第一个大块之后,用相同的手法填第二个大块,以此类推。如果发现填某个大块的时候,比如该大块长度为 k k k,但发现挑不出 k k k个未使用的不同字母了,则不存在方案,返回空串。这个算法其实是这么个意思,每次挑选的字母都是还剩余的字母中,出现次数最多,前面 k k k距离范围内没有,并且字典序最小的字母,进行拼接。一旦发现挑不出来满足条件的了,就说明无解,返回空串。
算法正确性证明:
首先,如果算法返回一个非空串,则必然存在合法方案,这一点是显然的(因为算法已经构造出来了)。下面证明如果存在合法方案,则一定不会返回空串。如果
l
≤
k
l\le k
l≤k,存在合法方案意味着每个字母只出现
1
1
1次,显然算法不会返回空串,结论成立。假设对于
l
<
n
l<n
l<n结论都是对的,对于长度为
n
n
n的情形。首先因为存在一个合法方案,设合法方案的排列是
s
=
a
1
a
2
.
.
.
a
n
−
1
a
n
s=a_1a_2...a_{n-1}a_n
s=a1a2...an−1an,我们考虑
a
n
a_n
an在原串中的出现次数。首先,如果
a
n
a_n
an不是出现次数最多(或最多之一)的字母,那么考虑
s
s
s中出现次数最多的字母
x
x
x,一定存在某个位置的
x
x
x使得
a
n
a_n
an与
x
x
x调换之后仍然是合法方案(如果不存在,则说明
a
n
a_n
an无论与哪个位置的
x
x
x调换都会有冲突,这其实与
a
n
a_n
an不是出现次数最多之一这个条件矛盾了),所以我们不妨设
a
n
a_n
an是出现次数最多或最多之一的字母,那么显然去掉最后一个字母后,
a
1
a
2
.
.
.
a
n
−
1
a_1a_2...a_{n-1}
a1a2...an−1也是满足条件的合法方案,由归纳假设,可以用贪心法构造出另一个”更规范“的合法方案
s
′
s'
s′,但是这里,我们强行定义
a
n
a_n
an这个字母的字典序是排最后的,用这个字典序去构造贪心的方案
s
′
s'
s′。接着把
a
n
a_n
an放在
s
′
s'
s′最前面,这样也得到了一个合法串,去掉前
k
k
k个字符后,之后的串也是合法的,由归纳假设,它们也可以重排成一个”更规范“的合法方案,但这一次将
a
n
a_n
an的字典序强行规定为是最前的,连同前
k
k
k个字符所得的串,就是
s
s
s按照贪心所得的串(这里的贪心也是将
a
n
a_n
an看成字典序排第一),所以不会返回空串,结论正确。
代码方面可以用最大堆来实现。代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
public String rearrangeString(String s, int k) {
// 注意要对k等于0或1特殊考虑
if (k <= 1) {
return s;
}
// 求一下每个字母出现了多少次
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++;
}
// 出现次数大的优先,出现次数一样则字典序小的优先
PriorityQueue<Character> maxHeap = new PriorityQueue<>((c1, c2) -> {
if (count[c1 - 'a'] != count[c2 - 'a']) {
return -Integer.compare(count[c1 - 'a'], count[c2 - 'a']);
}
return Character.compare(c1, c2);
});
for (char ch = 'a'; ch <= 'z'; ch++) {
if (count[ch - 'a'] > 0) {
maxHeap.offer(ch);
}
}
StringBuilder sb = new StringBuilder();
List<Character> list = new ArrayList<>();
int len = s.length();
while (!maxHeap.isEmpty()) {
// k是当前填的大块的长度
k = Math.min(len, k);
for (int i = 0; i < k; i++) {
if (!maxHeap.isEmpty()) {
char cur = maxHeap.poll();
sb.append(cur);
count[cur - 'a']--;
len--;
// 如果没用完,则用list记下来
if (count[cur - 'a'] > 0) {
list.add(cur);
}
} else {
return "";
}
}
maxHeap.addAll(list);
list.clear();
}
return sb.toString();
}
}
时空复杂度 O ( l ) O(l) O(l)。