一:题目
问题描述
回文串,是一种特殊的字符串,它从左往右读和从右往左读是一样的。小龙龙认为回文串才是完美的。现在给你一个串,它不一定是回文的,请你计算最少的交换次数使得该串变成一个完美的回文串。
交换的定义是:交换两个相邻的字符
例如mamad
第一次交换 ad : mamda
第二次交换 md : madma
第三次交换 ma : madam (回文!完美!)
输入格式
第一行是一个整数N,表示接下来的字符串的长度(N <= 8000)
第二行是一个字符串,长度为N.只包含小写字母
输出格式
如果可能,输出最少的交换次数。
否则输出Impossible
样例输入
5
mamad
样例输出
3
二:分析
这道题目首先拿到手,由于我比较菜,没有什么思路,参考了一下网上别人的思路,普遍都是用两个for循环实现前后同时遍历,但是我比较疑惑的是,他们的字符串是怎么变化的,貌似并没有随着每次的“移动次数”增加而动态变化。
(https://blog.csdn.net/liuchuo/article/details/51990430)
(https://blog.csdn.net/xiao_bai_9527/article/details/78461178)
我的方法:
我的核心函数fun中是把奇数长度和偶数长度字符串情况分开来。这样做是为了区别遇到“无法变为回文字符串的字符串”时的操作。其他部分操作是类似的:
首先需要用两次for循环前后遍历字符串,由于我的方法是让字符串每次变化后实时更新,所以其实第一层遍历只需要遍历到字符串的中间即可。
for(int i=0;i<n/2;i++)
{
for(int j=n-1-i;j>=i;j--)
{
if(i==j) //只要一次匹配不到,就不可能产生回文字符串
{
//********这部分后面再讨论
}
else if(s.charAt(i)==s.charAt(j))
{
numOfMove+=n-1-i-j; //从j移动到n-1-i位置
s=swap(s,i,j,n);
break;
}
}
}
如下图所示:i向后进时,方框外面的区域已经**“局部回文”了,故i只需要遍历到字符串中间就行了。
更新字符串的函数swap代码如下:
由于循环参数的设置,这个函数的i总会是小于j**,也就是只可能是右移字符的操作。
```
public static StringBuilder swap(StringBuilder sb,int i,int j,int n) //字符顺序右移函数
{
StringBuilder stemp = new StringBuilder("");
stemp.append(sb.substring(0, j));
stemp.append(sb.substring(j+1, n-i));
stemp.append(sb.charAt(j));
stemp.append(sb.substring(n-i, n));
return stemp;
}
``
这里有两点注意:
①s.substring(int a,int b),返回的是从字符串中索引位置a到b-1的子串。
②substring函数中若a=b,返回的字符串是空字符串。
下面就只剩下一个问题了,当在遍历过程中出现“孤儿字符”该怎么办,上面我说到了,奇偶长度字符串我是分开处理的。对于偶长度字符串,只要出现一个孤儿字符,它就不可能变为回文字符串。而奇长度字符至多允许出现一个孤儿字符。
对于奇长度字符串,若出现了一个孤儿字符,不能直接用之前的swap函数,两者定义不相符,我的做法是:
由于循环参数的设置,孤儿字符的位置肯定在中间字符的左边,那么直接把孤儿字符挪动到中间位置不就可以了,这样移动次数就是两者位置相减。
这样做是由两点要求的:①字符串实时更新,不能用swap函数。②操作完成后先把i-1再跳出内循环(更新字符串后,由于孤儿字符前移了,前面的字符就会往后缩)。
for(int i=0;i<(n-1)/2;i++)
{
for(int j=n-1-i;j>=i;j--)
{
if(i==j)
{
flag++;
if(flag>1) //孤儿字符超过1个,不可能产生回文字符串
{
System.out.println("Impossible");
return;
}
numOfMove+=(n-1)/2-i;
s=new StringBuilder("").append(s.substring(0, i)).append(s.substring(i+1, (n+1)/2))
.append(s.charAt(i)).append(s.substring((n+1)/2, n));
i--; //由于把孤儿字符移动到字符串中间,当前i指向字符到中间之前的字符自然就往前挪动了一格
break;
}
else if(s.charAt(i)==s.charAt(j))
{
numOfMove+=n-1-i-j; //从j移动到n-1-i位置
s=swap(s,i,j,n);
break;
}
}
挪动部分的原理可以看看下图:
三:完整代码以及部分运行结果
import java.util.Scanner;
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
String s = in.next();
StringBuilder sb = new StringBuilder(s);
fun(sb);
}
public static void fun(StringBuilder s)
{
int numOfMove = 0; //移动次数
int n=s.length();
if(n%2==0) //字符串为偶长度
{
for(int i=0;i<n/2;i++)
{
for(int j=n-1-i;j>=i;j--)
{
if(i==j) //只要一次匹配不到,就不可能产生回文字符串
{
System.out.println("Impossible");
return;
}
else if(s.charAt(i)==s.charAt(j))
{
numOfMove+=n-1-i-j; //从j移动到n-1-i位置
s=swap(s,i,j,n);
break;
}
}
}
}
else //字符串为奇长度
{
int flag=0; //孤儿字符个数
for(int i=0;i<(n-1)/2;i++)
{
for(int j=n-1-i;j>=i;j--)
{
if(i==j)
{
flag++;
if(flag>1) //孤儿字符超过1个,不可能产生回文字符串
{
System.out.println("Impossible");
return;
}
numOfMove+=(n-1)/2-i;
s=new StringBuilder("").append(s.substring(0, i)).append(s.substring(i+1, (n+1)/2))
.append(s.charAt(i)).append(s.substring((n+1)/2, n));
i--; //由于把孤儿字符移动到字符串中间,当前i指向字符到中间之前的字符自然就往前挪动了一格
break;
}
else if(s.charAt(i)==s.charAt(j))
{
numOfMove+=n-1-i-j; //从j移动到n-1-i位置
s=swap(s,i,j,n);
break;
}
}
}
}
System.out.println(numOfMove+"\n"+s);
}
public static StringBuilder swap(StringBuilder sb,int i,int j,int n) //字符顺序右移函数
{
StringBuilder stemp = new StringBuilder("");
stemp.append(sb.substring(0, j));
stemp.append(sb.substring(j+1, n-i));
stemp.append(sb.charAt(j));
stemp.append(sb.substring(n-i, n));
return stemp;
}
}
题目其实没要求输出最后的回文字符串,最后去掉就可以了。