试题J 字串排序
-
【问题描述】
小蓝最近学习了一些排序算法,其中冒泡排序让他印象深刻。
在冒泡排序中,每次只能交换相邻的两个元素。小蓝发现,如果对一个字符串中的字符排序,只允许交换相邻的两个字符,则在所有可能的排序方案中,冒泡排序的总交换次数是最少的。
例如,对于字符串 lan 排序,只需要 1 次交换。对于字符串 qiao 排序,总共需要 4 次交换。
小蓝找到了很多字符串试图排序,他恰巧碰到一个字符串,需要 V 次交换,可是他忘了把这个字符串记下来,现在找不到了。
请帮助小蓝找一个只包含小写英文字母且没有字母重复出现的字符串,对该串的字符排序,正好需要 V 次交换。如果可能找到多个,请告诉小蓝最短的那个。如果最短的仍然有多个,请告诉小蓝字典序最小的那个。请注意字符串中可以包含相同的字符。 -
【输入格式】
输入的第一行包含一个整数V,小蓝的幸运数字。 -
【输出格式】
题面要求的一行字符串。
【样例输入】
4
【样例输出】
bbaa
【样例输入】
100
【样例输出】
jihgfeeddccbbaa
【评测用例规模与约定】
对于 30% 的评测用例, 1 ≤ V ≤ 20。
对于 50% 的评测用例, 1 ≤ V ≤ 100。
对于所有评测用例, 1 ≤ V ≤ 10000。
参考原文
- 分析
思路:把这题拆成两个部分,第一个部分是确定最小长度,第二个部分是确定最小字符序
一、
求能交换次数V的字符串的最小长度,也就是求长度为多少时,能交换次数大于等于V次
给出一个定义:交换的次数等于字符串的逆序对个数
例如ba,b比a大,逆序对个数是1,cba,b产生1个,a产生两个,逆序对是3。
于是很自然想到,在长度相同的情况下,逆序数越多,交换次数越多。为了保证逆序对尽可能的多,我们构造的字符串尽量是递减序列。
例如:
长度 len = 1时,我们构造的字符串要形如 a,逆序对个数为 0,交换次数为 0。
当长度 len = 2 时,我们构造的字符串要形如 ba,逆序对个数 1,交换次数为 1。
当长度 len=3 时,我们构造的字符串要形如 cba,逆序对个数为 3,交换次数为 3。
⋯
当长度 len=26时,我们构造的字符串得为 zyxwvutsrqponmlkjihgfedcba,逆序对个数为 325,交换次数为 325。
题目中的要求是V最大是10000,所以这个325是远远达不到要求的,所以势必有字符是要重复的。
- 那接下来我们考虑一个新的问题,在只有a,b,c的情况下,构造出一个长度为7,交换次数最多的字符串,怎么想呢?给定一个字符串,插入一个字符,针对每个插入的字符而言逆序对增加的个数=前面比该字符大的字符数量 + 后面比该字符小的字符数量。
每次我们只要插入当前字符串中字符个数最少的字符就好了
为什么呢?
因为字符串中字符个数最少的这个字符与它前面和后面的字符都构成逆序对,它本身个数少,说明其他字符多,那构成的逆序对个数也多
比如cbaa,插入个数少的b变成cbbaa,增加了1+2个逆序对
而如果插入的是a,变成cbaaa,只增加了2个逆序对
所以我们对于上面的问题,长度为7,有三种方案cccbbaa,ccbbbaa,ccbbaaa,字符序最小的话,当然是ccbbaaa了。 - 不过我们原来的问题是:在给定的长度为len的字符串,其最大逆序数个数是多少?为什么要求这个呢,因为当我们找到第一个长度len,其最大逆序数>=V时,我们就可以从这个长度产生字符序最小的字符串,而这个len也就是最小的长度。
- 那回到正题,有了上边的例子,我们明白了,一个长度为len的字符串里边,要达到最大逆序数,字符之间要逆序,每个字符个数最好是平均分配的,又要字符序最小,个数多的应该是字符顺序小的。字符数量为len/26+1(向下取整)的字符有len%26个,字符数量为len/26的字符有26-len%26个。那给定len,最大的交换次数的公式如下所示:
这个公式是分为两部分,即字母个数较少的字母构成的逆序对,和相比较字母个数多1的字母构成的逆序对,什么叫相比较字母个数多1?比如ccbbaaa,a就变比b和c的个数2多了1了。最多只会多1个,因为要尽可能地均匀,越均匀逆序对个数越大,比如说len=7时,gfedcba就比ccbbaaa的逆序对个数多(上面举的例子ccbbaaa限定了只能填abc这三个字母,所以和题目实际不同,只是作为假设。其实对于长度不超过26的字符串来说,它们是不可能有字母个数相比较多1的字母,直接按照逆序一个一个地排就好了,直到长度为27,才有zyx…cbaa,a多一个的情况)
先不看分母的除以2,分子部分其实是针对每一个字母的,就是只看这个字母前面比它大的和它构成逆序对,后面比它小的也构成逆序对
那这样算的话,会有重复
比如说长度为55的字符串zzyyxx…eeddcccbbbaaa
在单独看某一个y的时候,算了y和某一个x是一对逆序对,在单独看那一个x时候又会算y和x构成的这对逆序对
所以是两两之间会重复计算,所以要除以2
再来看看分子的两部分 - +左边的是相比较字母个数多1的字母构成的逆序对,也就是例子中的cccbbbaaa这一部分,每个字母其实都和除自己这个字母以外的所有其他字母构成逆序对
(len-(len/26+1))->(55-(55/26+1))=(55-3)=52,除自己这个字母以外,和剩下的所有其他字母构成逆序对,比如a就和zzyyxx…eeddcccbbb构成逆序对,就是减去a本身的3个a,剩下的数
(len/26+1)->(55/26+1)=3,相比较字母个数多1的字母它自己的个数,比如cba每个都有3个
(len%26)->(55%26)=3,相比较字母个数多1的字母有几个,a,b,c这有3个字母
它们相乘就是相比较字母个数多1的字母构成的逆序对个数 - +右边的是字母个数较少的字母构成的逆序对,也就是例子中zzyyxx…eedd这一部分,每个字母也都和除自己这个字母以外的所有其他字母构成逆序对
(len-(len/26))->(55-(55/26))=53,每个字符,和除自己这个字母以外的其他所有字母都构成逆序对比如z就和除zz以外的剩下53个字母都构成逆序对
(len/26)->(55/26)=2,不多余重复的字母的个数,即zzyyxx…eedd,zyx…ed每个字母都有2个
(26-len%26)->(26-55%26)=(26-3)=23,这是不多余重复的字母个数,即zyx…ed这23个字母
它们相乘就是字母个数较少的字母构成的逆序对个数 - 按照这个计算方法,就能计算出至少多长的字符串长度能构成V个逆序对个数
二、
确定如何放字母能得到最小字符序
在已经的到的len长度,从前往后依次放字母,每个位置都尝试把a-z这26个字母放进去试试
如果当前位置能放字母j,就确定这个位置放j,然后继续尝试放下一个位置
怎么判断当前位置能不能放j?
在当前位置放j后,预测一下后面的放法,如果后面有放法能使得最后所有的逆序对个数大于等于V,说明这个位置可以放j
#include<bits/stdc++.h>
using namespace std;
int V , len , now , cnt[27] , sum[27];
int get_max(int len){
return ((len - (len / 26 + 1)) * (len / 26 + 1) * (len % 26) + (26 - len % 26) * (len / 26) * (len - len / 26)) / 2;
}
bool check(int x , int n){
//当前位置i放x字母后,后面的n个位置是否符合要求
memset(cnt , 0 , sizeof(cnt));//cnt记录后面n个位置放置的对应字符数量
int add1 = 0 , add2 = 0;
for(int j = 26 ; j >= x + 1 ; j --) add1 += sum[j