这东西有点难懂呀,搞了好久了。
题目介绍
小蓝最近学习了一些排序算法,其中冒泡排序让他印象深刻。
在冒泡排序中,每次只能交换相邻的两个元素。小蓝发现,如果对一个字符串中的字符排序,只允许交换相邻的两个字符,则在所有可能的排序方案中,冒泡排序的总交换次数是最少的。
例如,对于字符串 lan 排序,只需要 1 次交换。对于字符串 qiao 排序,总共需要 4 次交换。
小蓝找到了很多字符串试图排序,他恰巧碰到一个字符串,需要 V 次交换,可是他忘了把这个字符串记下来,现在找不到了。
请帮助小蓝找一个只包含小写英文字母且没有字母重复出现的字符串,对该串的字符排序,正好需要 V 次交换。如果可能找到多个,请告诉小蓝最短的那个。如果最短的仍然有多个,请告诉小蓝字典序最小的那个。请注意字符串中可以包含相同的字符。
【输入格式】
输入的第一行包含一个整数V,小蓝的幸运数字。
【输出格式】
题面要求的一行字符串。
【样例输入】
4
【样例输出】
bbaa
【样例输入】
100
【样例输出】
jihgfeeddccbbaa
简单分析
具体还是结合代码吧,注解上有点。
首先我们得得到最短能搞到的长度。
我们看什么时候能最大呢。全部逆序肯定是最大的,如cba,但是就算是26个全部逆序了也不一定到的了题目给的数字,所以我们需要重复元素。
重复的话怎么样才是最大呢。我们看cba(3)中加一个重复会发现都是一样的,假设我们ccba(5)了,在加一个发现加c是最少的(7)而加b/a可以到(8)
我们在计算中发现,增加值就等于左边比他大的加上右边比他小的,所以很容易的就可以得到平均起来会是最大的,
那么我们可以获取最大的长度了。假定长度为len
由于平分最大,那么我们可以知道每一个字母的数量只可能是len/26和len/26+1这2种情况。
且len/26+1的字母有len%26个。
所以我们可以算取长度所能取得的最大次数可以得到:
每一个字母个数乘以除这个字母以外的所有字母长度和/2 = 次数
因为ba对b,ba是那么对a,ba也是所以除以2,又因为逆序排列,所以可以和其他所有的组合起来
那么就可以得到次数为
次数=(len/26字母以外的个数)×(字母种类个数)×(每一个字母的个数)+…同理
次数 = ( ( l e n − l e n % 26 ) ∗ ( l e n % 26 ) ∗ ( l e n / 26 + 1 ) + ( l e n − l e n / 26 ) ∗ ( 26 − l e n % 26 ) ∗ ( l e n / 26 ) ) ) / 2 次数= ((len - len\%26)*(len\%26)*(len/26+1)+ (len - len / 26) * (26 - len \%26) * (len / 26)) )/ 2 次数=((len−len%26)∗(len%26)∗(len/26+1)+(len−len/26)∗(26−len%26)∗(len/26)))/2
接下来
我们只需要遍历每一个位置,检查是否可以放入这个元素,知道最后就可以了。
那么我们的问题就变成了,如何看能不能放这个元素
我们需要维护2个全局变量。
一个是这个元素之前已经放入的各个字母的个数
a:一个是这个元素之前所得到的逆序数。
b:当放入这个字母的时候,我们可以计算出其增加的逆序数为比他大的字母数量的和
c:然后来遍历后面的所有可能,计算其增加的逆序数。
如果这a+b+c大于等于需要的则是可以的,不行则回溯当前位置。
最后的难点就是算后面可能增加的最大逆序数了
在这个步骤中,每个位置我们从最大的z开始遍历。
序列数的计算我们需要得到前面比他大的字母个数就可以了。
具体看代码吧。
代码
package com.yu;
import java.io.*;
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int v;
static int[] sum = new int[27];
//记录当前所获得的所有逆序队列
static int num = 0;
static int len = 1;
public static void main(String[] args) throws IOException {
v = nextInt();
while (get_len(len) < v)
len++;
System.out.println(len);
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= len; i++) {
for (int j = 1; j < 27; j++) {
if (check(i, j)) {
sb.append((char) ('a' + j - 1));
break;
}
}
}
System.out.println(sb);
}
//在i位置放j可不可行
private static boolean check(int i, int j) {
// i 位置和以前对构成的所有的逆序队列。
int pre = Arrays.stream(sum, j + 1, 27).sum();
//假定i位置放j
sum[j]++;
// 此时我们还需要获取后面所能得到的最大逆序队列。
int add = 0;
int[] now = new int[27];
// 遍历后面所有位置
for (int n = i + 1; n <= len; n++) {
// 已经加上的逆序列
int addNum = -1;
// 比这个大的元素个数
int big = 0;
// 当前n位置元素
int e = 0;
// 遍历可能放的元素
for (int m = 26; m > 0; m--) {
// 逆序队列最大个数
// 最大就是从前面一直逆序 - 当前一样的元素
int z = n - i - 1 - now[m] + big;
if (z > addNum) {
addNum = z;
e = m;
}
// 加上i之前比他大的
big += sum[m];
}
now[e]++;
add += addNum;
}
// 判断是否可行
if (num + pre + add >= v) {
num += pre;
return true;
} else {
//回溯
sum[j]--;
return false;
}
}
private static int get_len(int len) {
int n = len / 26;
int m = len % 26;
return ((len - (n + 1)) * m * (n + 1) + (len - n) * (26 - m) * n) / 2;
}
}