题目描述:
回文串,是一种特殊的字符串,它从左往右读和从右往左读是一样的。有人认为回文串是一种完美的字符串。现在给你一个字符串,它不一定是回文的,请你计算最少的交换次数使得该串变成一个回文串。这里的交换指将字符串中两个相邻的字符互换位置,例如所给的字符串为mamad,第一次交换ad,得到mamda;第二次交换md,得到madma;第三次交换ma,得到madam (回文!完美!)
程序要求从键盘读入数据。第一行是一个整数N(N <= 8000),表示所给字符串的长度,第二行是所给的字符串,长度为N且只包含小写英文字母。如果所给字符串能经过若干次交换变成回文串,则输出所需的最少交换次数;否则,输出Impossible。
图a和b是两个实际运行的样例。
5
mamad
3
图a
6
aabbcd
Impossible
图b
我的分析(论证最少移动):
对一个字符串,我们用字符数组来表示,如果字符串能移动成一个回文串,那么串中字符应该是按某一个位置对称的,也就是说两个相同的字符的位置经过移动后是相对确定的。
我们不妨假设现在有奇数个字符(这里假设只是为了讨论方便,奇数个字符在数组中可以找到一个对称的点,偶数个字符的情况一样的,只是对称点不在数组中,而是在数组最中间两个值的中间),假设现在字符的长为n,那么对称点为(n+1)/2,先又假设现在有两个相同的字符的位置分别为i,j,显然不管目前他们的位置如何,他们经过移动后的位置一定满i+j=n+1,我们移动这两个字符可以看出,首先我们选择一个距离对称点远一些的字符,当我们保持这个字符的位置不改变,而将另一个移动到它的对称位置上我们移动的步数是最少的(这里如果两个都同时移动,如果都向远离对称点的位置移动,显然步数会增加,如果都向靠近对称点的方向移动,到最后他们保持对称的时候他们移动的步数最好的情况也只是上面最少的情况,不是最好的情况我们后面说),所以我们选择保持一个字符的位置不变,去移动另一个相同的来解决问题。
现在考虑选择字符顺序的问题,我们可以看到对一个串来说,当最外面的对称的两个已经排好后,再移动内层的就不影响外面已经排好的了,也就是说再不需要和外层的相互移动了,而且把该移动到内层的也移动到内层去了,所以我们选择从外层往内去选择那个不动的字符是合理的(函数中选择的是从第一个到对称点的顺序,当然从最后一个开始是一样的)。相反我们从内层往外选择,如果两个相同的字符在对称点的一边时,那么固定靠近内的一个,移动靠外的一个,则这两个相同的也相互移动了一次,这是浪费的,所以我们上面提到的选择距离对称点远一些的也是这个道理,当然从外往内选就可以避免这个问题。
好,我们固定了前端一个之后,那么我们选择另外一个则总从最后开始找,找到这个相同的之后就往后移动到与之对称的位置,这样一直循环到对称点左右的位置,回文串也就移动好了,而且这就是最少的步数。
当然还有一个特殊情况,如果是奇数个字符时,如果单个的那个字符(移动到最后即为对称点)在对称点的左边,那么我们选择的从左向右的时候,这个因为是单个没有另一个相同的所以它就没移动(不影响其他字符的移动,也不影响步数),所以最后等其他的移动好之后,我们再把这个字符移动到最中间就可以了,当然如果这个字符在对称点的右边的时候,就没有这个情况(其实这种情况我们只要再把最后一个当做第一个固定的点反过来从右到左进行上面的移动过程也是可以的,这样左端那个单个的点照样能移动到最中间)。
对一个字符串,我们用字符数组来表示,如果字符串能移动成一个回文串,那么串中字符应该是按某一个位置对称的,也就是说两个相同的字符的位置经过移动后是相对确定的。
我们不妨假设现在有奇数个字符(这里假设只是为了讨论方便,奇数个字符在数组中可以找到一个对称的点,偶数个字符的情况一样的,只是对称点不在数组中,而是在数组最中间两个值的中间),假设现在字符的长为n,那么对称点为(n+1)/2,先又假设现在有两个相同的字符的位置分别为i,j,显然不管目前他们的位置如何,他们经过移动后的位置一定满i+j=n+1,我们移动这两个字符可以看出,首先我们选择一个距离对称点远一些的字符,当我们保持这个字符的位置不改变,而将另一个移动到它的对称位置上我们移动的步数是最少的(这里如果两个都同时移动,如果都向远离对称点的位置移动,显然步数会增加,如果都向靠近对称点的方向移动,到最后他们保持对称的时候他们移动的步数最好的情况也只是上面最少的情况,不是最好的情况我们后面说),所以我们选择保持一个字符的位置不变,去移动另一个相同的来解决问题。
现在考虑选择字符顺序的问题,我们可以看到对一个串来说,当最外面的对称的两个已经排好后,再移动内层的就不影响外面已经排好的了,也就是说再不需要和外层的相互移动了,而且把该移动到内层的也移动到内层去了,所以我们选择从外层往内去选择那个不动的字符是合理的(函数中选择的是从第一个到对称点的顺序,当然从最后一个开始是一样的)。相反我们从内层往外选择,如果两个相同的字符在对称点的一边时,那么固定靠近内的一个,移动靠外的一个,则这两个相同的也相互移动了一次,这是浪费的,所以我们上面提到的选择距离对称点远一些的也是这个道理,当然从外往内选就可以避免这个问题。
好,我们固定了前端一个之后,那么我们选择另外一个则总从最后开始找,找到这个相同的之后就往后移动到与之对称的位置,这样一直循环到对称点左右的位置,回文串也就移动好了,而且这就是最少的步数。
当然还有一个特殊情况,如果是奇数个字符时,如果单个的那个字符(移动到最后即为对称点)在对称点的左边,那么我们选择的从左向右的时候,这个因为是单个没有另一个相同的所以它就没移动(不影响其他字符的移动,也不影响步数),所以最后等其他的移动好之后,我们再把这个字符移动到最中间就可以了,当然如果这个字符在对称点的右边的时候,就没有这个情况(其实这种情况我们只要再把最后一个当做第一个固定的点反过来从右到左进行上面的移动过程也是可以的,这样左端那个单个的点照样能移动到最中间)。
我的解答(C++程序):
#include "stdio.h"
#include"iostream.h"
int n,t=0,g=0;
char *b;
#include"iostream.h"
int n,t=0,g=0;
char *b;
int huiwen(char a[])
{//判断是否有可能移动成回文数;
int dange=0;
for(int i=0;i<n;i++)
{ int flag=1;
for(int p=0;p<i;p++)
{
if(a[p]==a[i]) {flag=0;break;}
}
if(flag)
{
int sum=1;
for(int j=i+1;j<n;j++)
{
if(a[j]==a[i])
sum++;
}
if(sum%2)
{ dange++;
if(dange>=2)
{return 0;}
}
}
}
{//判断是否有可能移动成回文数;
int dange=0;
for(int i=0;i<n;i++)
{ int flag=1;
for(int p=0;p<i;p++)
{
if(a[p]==a[i]) {flag=0;break;}
}
if(flag)
{
int sum=1;
for(int j=i+1;j<n;j++)
{
if(a[j]==a[i])
sum++;
}
if(sum%2)
{ dange++;
if(dange>=2)
{return 0;}
}
}
}
return 1;
}
}
void swap(char *a,char *d)
{//交换一次两个相临的字符;
char c=*a;
*a=*d;
*d=c;
g++;
}
{//交换一次两个相临的字符;
char c=*a;
*a=*d;
*d=c;
g++;
}
void main()
{
cout<<"请输入字符串的长度:"<<endl;
cin>>n;
b=new char[n];
cout<<"请输入字符串:"<<endl;
cin>>b;
if(!huiwen(b))
cout<<"Impossible"<<endl;
else
{
for(int i=0;i<(n-1)/2;i++)
{
if(b[n-1-t]!=b[i])
{
for(int j=n-1-i;j>i;j--)
if(b[i]==b[j])
{
t++;
for(int p=j;p<n-t;p++)
{
char *pa,*pb;
pa=&b[p];
pb=&b[p+1];
swap(pa,pb);
}
break;
}
}
else t++;
}
for(int q=0;q<(n-1)/2;q++)
{
int z=n-1-q;
if(b[q]!=b[z])
{
char *pa,*pb;
pa=&b[q];
pb=&b[q+1];
swap(pa,pb);
}
}
cout<<"最少移动次数:"<<g<<endl;
cout<<"经过移动后的回文串:"<<endl;
cout<<b;
cout<<endl;
}
}
{
cout<<"请输入字符串的长度:"<<endl;
cin>>n;
b=new char[n];
cout<<"请输入字符串:"<<endl;
cin>>b;
if(!huiwen(b))
cout<<"Impossible"<<endl;
else
{
for(int i=0;i<(n-1)/2;i++)
{
if(b[n-1-t]!=b[i])
{
for(int j=n-1-i;j>i;j--)
if(b[i]==b[j])
{
t++;
for(int p=j;p<n-t;p++)
{
char *pa,*pb;
pa=&b[p];
pb=&b[p+1];
swap(pa,pb);
}
break;
}
}
else t++;
}
for(int q=0;q<(n-1)/2;q++)
{
int z=n-1-q;
if(b[q]!=b[z])
{
char *pa,*pb;
pa=&b[q];
pb=&b[q+1];
swap(pa,pb);
}
}
cout<<"最少移动次数:"<<g<<endl;
cout<<"经过移动后的回文串:"<<endl;
cout<<b;
cout<<endl;
}
}
下面是别人的解答:
#include <iostream>
using namespace std;
using namespace std;
int main()
{
int N; // 字符串长度
cin >> N;
char s[8001] = {0}; // 保证可存下结尾的'/0'
cin >> s;
{
int N; // 字符串长度
cin >> N;
char s[8001] = {0}; // 保证可存下结尾的'/0'
cin >> s;
// 判断是否可变为回文串
int b[26] = {0}; // 记录'a'~'z'出现的次数
for (int i = 0; i < N; i++)
b[s[i] - 'a']++; // 相应字母出现次数加一
int odd = 0; // 有多少个字母出现奇数次
char charodd = '/0'; // 出现奇数次的字母
for (int i = 0; i < 26; i++)
if (b[i] % 2 == 1) // b[i]是奇数
{
odd++;
charodd = i + 'a'; // 记录该字母
}
if (odd > 1)
cout << "Impossible" << endl; // 输出
else
{
int change = 0; // 交换次数
for (int i = 0; i < N/2; i++) // 依次考虑左侧的字母
{
if (s[i] == charodd) // 是charodd,转而考虑右侧字母
{
int j = 0;
for (j = i; j <= N-i-1; j++) // 从左侧该位置开始,找相同字母
if (s[j] == s[N-i-1]) // 找到
break;
change += j - i; // 需要j-i次移动可到左侧位置
for (int k = j; k > i; k--) // 实现字母的移动
s[k] = s[k-1];
s[i] = s[N-i-1];
}
else // 考虑左侧字母
{
int j = 0;
for (j = N-i-1; j >= i; j--) // 从右侧对称位置开始,找相同字母
if (s[j] == s[i]) // 找到
break;
change += N-i-1 - j; // 需要N-i-j-j次移动可到右侧位置
for (int k = j; k < N-i-1; k++) // 实现字母的移动
s[k] = s[k+1];
s[N-i-1] = s[i];
}
}
cout << change << endl; // 输出
}
return 0;
}
int b[26] = {0}; // 记录'a'~'z'出现的次数
for (int i = 0; i < N; i++)
b[s[i] - 'a']++; // 相应字母出现次数加一
int odd = 0; // 有多少个字母出现奇数次
char charodd = '/0'; // 出现奇数次的字母
for (int i = 0; i < 26; i++)
if (b[i] % 2 == 1) // b[i]是奇数
{
odd++;
charodd = i + 'a'; // 记录该字母
}
if (odd > 1)
cout << "Impossible" << endl; // 输出
else
{
int change = 0; // 交换次数
for (int i = 0; i < N/2; i++) // 依次考虑左侧的字母
{
if (s[i] == charodd) // 是charodd,转而考虑右侧字母
{
int j = 0;
for (j = i; j <= N-i-1; j++) // 从左侧该位置开始,找相同字母
if (s[j] == s[N-i-1]) // 找到
break;
change += j - i; // 需要j-i次移动可到左侧位置
for (int k = j; k > i; k--) // 实现字母的移动
s[k] = s[k-1];
s[i] = s[N-i-1];
}
else // 考虑左侧字母
{
int j = 0;
for (j = N-i-1; j >= i; j--) // 从右侧对称位置开始,找相同字母
if (s[j] == s[i]) // 找到
break;
change += N-i-1 - j; // 需要N-i-j-j次移动可到右侧位置
for (int k = j; k < N-i-1; k++) // 实现字母的移动
s[k] = s[k+1];
s[N-i-1] = s[i];
}
}
cout << change << endl; // 输出
}
return 0;
}
好象大概没多少问题,哈哈;