力扣算法day44-2
双周赛T4 - 2193-得到回文串的最少操作次数
题目
代码实现
class Solution {
public int minMovesToMakePalindrome(String s) {
// 贪心 速度:33ms 这个暴力遍历版本速度较慢,树形数组版本明天发周赛后两道的时候补发
// 题目分析:思路参考大佬零神,零神没有java版本
// 看了题解后的个人理解:
// 1.首先是找规律,看一会比较容易看到的规律是回文串两边对称,而且题目直接说确保s一定能变成一个回文串,
// 那么这个对称就是一个可以利用的点。而这里这个规律引出来的思路,就是后面题解一定要注意理解本题最重要
// 的地方,也是必须要理解的地方,就是相对顺序。如果哪里懵,就多想想相对顺序这个基础。
// 以下先以偶数情况说明:
// (1) 首先,根据这个对称性,可以知道经过多次交换后,如果得到了回文串,那么每个字符的前面一半一定都在
// 前半段,后面一半都在后半段。
// (2) 相同的字母是不需要交换位置的,交换了反而增加步骤。它们之间相对顺序也就是不需要动的。(这个应该很好理解)
// (3) 那么由(1)可得,我们可以将字符先分为两组,一组是最后应该在前半段的,二组是最后应该在后半段的,那么
// 一组二组长度各占一半。然后,我们可以统一每个字母出现的次数,比如一个字母出现了4次,那么前两次就在
// 一组,后两次就在二组。由(1)我们知道了一组都需要在前半段,二组都需要在后半段,所以按照贪心思路,我们
// 需要用最小的步骤将一组交换到前一半,二组交换到后一半,那么再次遍历的时候将遇到的第一个一组的交换到
// 坐标0,第二个遇到的第一组元素交换到坐标1就是最小步骤,如果交叉了,那么步骤会增加。
//
// 这里(3)需要理一下,首先为什么要分成2个组?
// 分为两个组有两个原因,一是分的这两个组的定义是最终结果需要相对位置,二是分为两个组后,两个组自身的相对位置
// 在不同组元素之间发生交换的话是不会发生变化的。那么就可以用贪心思路得出两个组定义下的最小操作次数。
// 为什么要求这个最小?
// 因为最小步骤最后结果一定是字符前一半在前面,后一半在后面,所以应该在前面的和应该在后面的两个元素相互交换是
// 一定是满足最小步骤的定义的(没有更小且步骤更小的原子情况了)!!!! 而(3)中按照遍历的时候将遇到的第一个一组的
// 交换到坐标0,第二个遇到的第一组元素交换到坐标1,这里的交换步骤就是这种情况,不用我继续解释了吧!
//
// 那么最后就是化为了求一组,二组逆序顺序相同 的最小步骤了,求出这个步数加上上面贪心求出来的一组遍历的步数就
// 最终的最小步数(因为这里一组二组知识每个字母的字母之间的相对位置正确,但是一组、二组内的元素是否符合回文串
// 还需要它们满足一组、二组互为逆序顺序,那就是求一组、二组 交换成逆序顺序的最小步骤)。
//
// 求二组变成一组的逆序顺序的最小步骤方法同样是贪心,因为具体是求一组变为二组还是二组变为一组的逆序顺序是一样的
// 它们就是呈镜像的,两边的操作步数是相同的,自己可以模拟一下,就像是反的一样。我这里就选用二组化一组。
// 就是直接将一组字母坐标给二组字母赋值,那么二组的字母就变成了0,1,2,3之类的数字,而不走直接就等于冒泡排序求
// 升序(这里求升序,所以依照结构需要逆序赋值坐标)的次数即可。(注:因为是逆序嘛,所以求坐标倒过来嘛。)
//
// 最后,对于奇数,和偶数只有一点不同,就是奇数多出来的那一个按照偶数的求法会被分配到二组中去,然后顺序随机,而
// 回文串的话奇数多出来那个一定是在中间,所以一组最后加上一个这个字母,那么二组那个多出来的奇数字母按照一组进行
// 逆序交换的时候一定会被交换到最前面去的。相当于一个巧办法。
// 注:这里奇数这个多出来的字母不会影响前面的结果,还是那句话,切记理解相对顺序不变这个点,就是因为相对顺序不变
// 所以,奇数多出来的字符被分到二组它本身的相对顺序也是没变的,没得啥影响。后面的逆序排序求出来的就是最小步数,
// 前面加一个这个字符就让它会被换到合适的位置,也就是正确的计算结果。
int result = 0;
// 首先遍历统计字符出现的数量,这里速度考虑就用哈希表了。
int[] letter = new int[26];
char[] sChar = s.toCharArray();
for(int i = 0;i < sChar.length;i++){
letter[sChar[i] - 'a']++;
}
// 将一组的元素依次放入前半段,二组放入后半段,这里具体实现考虑后面还要求一组对二组的逆序顺序相同的最小步数。
// 而且这个的次数可以直接算法出来,所以就直接将前半段和后半段造两个数组来装了。方便后面遍历。
int lIndex = 0;
int rIndex = 0;
int[] lArray = new int[(sChar.length+1) / 2];
int[] rArray = new int[(sChar.length+1) / 2];
int[] letterJ = new int[26];
for(int i = 0;i < sChar.length;i++){
if(letterJ[sChar[i]-'a'] < letter[sChar[i]-'a'] / 2){
letterJ[sChar[i]-'a']++;
lArray[lIndex] = sChar[i];
result += i - lIndex;
lIndex++;
} else{
rArray[rIndex] = sChar[i];
rIndex++;
}
}
// 如果为奇数,需要在lArray最后加上一个这个奇数字母。
if(sChar.length % 2 == 1){
for(int i = 0;i < letter.length;i++){
if(letter[i] % 2 == 1){
lArray[lArray.length - 1] = 'a'+i;
letterJ[i]++;
break;
}
}
}
System.out.print((char)lArray[lArray.length-1]);
// 将一组的元素和二组元素匹配,并赋值一组坐标给二组。
int[] sort = new int[lArray.length];
for(int i = 0;i < lArray.length;i++){
int index = 1;
for(int j = 0;j < rArray.length;j++){
if(lArray[i] == rArray[j]){
if(letterJ[lArray[i]-'a'] == index){
letterJ[lArray[i]-'a']--;
sort[j] = lArray.length - 1 - i;
break;
} else{
index++;
}
}
}
}
for(int i = 0;i < sort.length - 1;i++){
for(int j = i+1;j < sort.length;j++){
if(sort[i] > sort[j]){
result++;
}
}
}
return result;
}
}