首页 > 代码库 > [BASIC-19] 完美的代价

[BASIC-19] 完美的代价

基础练习 完美的代价  
时间限制:1.0s   内存限制:512.0MB
问题描述
  回文串,是一种特殊的字符串,它从左往右读和从右往左读是一样的。小龙龙认为回文串才是完美的。现在给你一个串,它不一定是回文的,请你计算最少的交换次数使得该串变成一个完美的回文串。
  交换的定义是:交换两个相邻的字符
  例如mamad
  第一次交换 ad : mamda
  第二次交换 md : madma
  第三次交换 ma : madam (回文!完美!)
输入格式
  第一行是一个整数N,表示接下来的字符串的长度(N <= 8000)
  第二行是一个字符串,长度为N.只包含小写字母
输出格式
  如果可能,输出最少的交换次数。
  否则输出Impossible
样例输入
5
mamad
样例输出
3
分析:

1、我们先来分析一下输出是“Impossible”的情况:

        ①形如“mama”,每个字符出现次数都是偶数次,可以得到完美回文串“maam”或者“amma”

        ②形如“mamad”,只有1个字符出现次数是奇数次,其余的都是偶数次,可以得到完美回文串“madam”或者“amdma”

        ③形如“mamade”,有2字符出现次数是奇数次,取余的都是偶数次,得不到完美回文串

        以此类推,我们可以发现,字符串中的“出现奇数次的字符”大于等于2个的时候,是不能得到完美回文串的,这时输出是“Impossible”

2、我们再来分析一下可以得到完美回文串的情况:

        首先明确一个观念:交换两个相邻字符串,实际上就是对某一个字符进行左移或右移。

        将原字符串重组为一个回文串并不是难事,可以想到不同的方法,但是哪一种方法的交换次数最少呢,或者说如何保证某一种方法的交换次数一定最少呢?我们联想到题目中的字母交换操作很像是冒泡排序中交换两个相邻数的位置,要保证交换次数最少,就是要保证这样的冒泡操作都是“正确”的。这种“正确”,是符合某种准则即某种算法原理的。

      我们来看看应该如何进行交换操作,或者说如何移动字母(下文都直观的称为移动),才可以保证次数最少。

      见图1,其中X表示是串中任意的字母,A表示在移动次数最少的回文串中,最终处于某对称位置的两个相同字母。

     (图1)

      对于图1中的串来说,任何涉及字母A的交换操作,我们都视为对A进行移动。因此要让移动次数最小,每一个字母A都应该只进行单方向的移动。这一对字母A的最终位置可以有多个选项,图1中对称位置1是最靠近中心的位置,对称位置2是最远离中心的位置,但是所有可能的最终位置都是需要将A向左移动4次(移动单个A的次数,或移动两个A的次数之和)。

      图1中的两个A分布在中心的两侧,如果分布在中心的同侧,如图2所示,结论也是类似的。这一对字母A的最终位置可以有多个选项,图2中对称位置1是最靠近中心的位置,对称位置2是最远离中心的位置,但是所有可能的最终位置都是需要将A向右移动14次(移动单个A的次数,或移动两个A的次数之和)。注意,在移动中左边的A不能越过右边的A,否则相当于一个A向左移,一个A向右移。

      (图2)

      那么如何选择实际串中的字母为我们的A呢?见图3,假设初始串中,左侧A的对称位置是字母B。先将右侧的A移到右侧B的位置,再移动两个B到对称位置,与先移动左侧B到左侧A的位置,再移动两个A到对称位置,移动次数是一样的。首先,后移动的一对字母在移动过程中,不会与前一对已到位的字母进行交换。这是因为,移动后一对字母时,只能按单个方向移动,如果在移动过程中与前一对字母发生交换,那么相当于前一对字母向相反方向进行了移动,而它们原来位置是对称的,这样移动后就不可能还对称了。因此,可认为两对字母的移动是独立的,总的移动次数是两对字母的移动次数之和,而每一对字母的移动次数与可能的位置无关,因此先移动哪一个字母并没有关系。

      (图3)

      为了保证后移动的字母不会与前面已经移动过的字母发生交换,我们可以从原始字符串的最左边开始,依次选择各个字母为当前移动的那对字母,而且就以该字母的位置为最终的对称位置,而这对字母的另一个,当然就选取最右侧对称位置开始向左第一个出现的相同字母。如果字符串的长度为偶数,则所有字母出现次数都是偶数,以上过程可一直进行下去,在移动倒数第二对字母到位后,最后一对字母也同时到位,得到结果的回文串。如果字符串的长度为奇数,则有可能在这一过程中提前碰到最终应该放在串中间的那个字母。根据前面的分析,不能马上将它放到串的中间,因为并不能保证它不会与其后的字母发生交换。这时我们可以转而考虑右边对称位置的字母,将它作为当前考虑的那一对,在左侧当前位置向右查找第一个相同字母,进行移动。

      以上的算法已经基本清晰了,还差一点小细节,我们怎么知道当前位置的字母就是那个最终应该放在串中间的那个字母呢?首先这个字母必然是那个唯一出现次数为奇数的字母,我们可以在判断原串是否可以变成回文串时顺便记录下来。其次,由于这个字母可以在原串出现若干次,所以并不是第一次碰到的该字母就一定是最终移动到串中间的那个字母。假设该字母共出现n次,则应该是第(n+1)/2次碰到的那个字母最终放在串中间。这一过程需要记录该字母已出现过几次,我们可以变通一下对该字母的处理,方法如下:只要碰到该字母,就以对称位置的字母为当前要移动的那对字母。实际上,若对称位置也是该字母,那么正好已到位。

      根据以上分析得到的算法,我们能够以最少的移动次数,也就是最少的交换次数,将原字符串变为一个回文串,只要累加每一对字母所移动的次数就得到原题目要求的答案。

      下面我们来估算一下,对于一个长度为N的字符串,最坏情况下至少要交换多少次字母。根据前面的算法,最坏情况是每一对字母都需要移动最多的位置,因此这样的一个穿看起来是aabbccddee…则第一个字母需要移动N-2个位置,第二个字母需要移动N-4个位置,依次类推。当N=2k(k>=1)时,只需要移动k-1对字母,总移动次数为:

(N-2)+(N-4)+…+2=(2k-2)+(2k-4)+…+2=2k*(k-1)/2=k(k-1)

当N=2k-1时,同样需要移动k-1对字母,总移动次数为:

(N-2)+(N-4)+…+1=(2k-3)+(2k-5)+…+1=(2k-2)*(k-1)/2=(k-1)2

由于题目中N不超过8000,即k<=4000,所以对于以上的推导的公式,总的移动次数都不超过int的表示范围。当然,实际上当N很大时,不可能每个字母只出现2次,总的移动次数可以估算为更小的值。

import java.util.Scanner;

public class Main {

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);

		while (scanner.hasNext()) {
			int n = Integer.parseInt(scanner.nextLine());

			String str = scanner.nextLine();
			char[] chs = str.toCharArray();

			int[] count = new int[26];
			char ch = '0';
			int oddchar = 0;

			for (int j = 0; j < chs.length; j++) {
				int index = chs[j] - 'a';
				count[index]++;
			}

			for (int i = 0; i < 26; i++) {
				if (count[i] % 2 != 0) {
					oddchar++;
					ch = (char) (i + 'a');
				}
			}

			if (oddchar > 1) {
				System.out.println("Impossible");
			} else {
				int result = exchange(chs, n, ch);
				System.out.println(result);
			}
		}
	}

	private static int exchange(char[] chs, int n, char ch) {
		int count = 0, i, j, k;
		for (i = 0; i < n / 2; i++) {
			if (chs[i] == ch) {
				for (j = i; j < n - i - 1; j++) {
					if (chs[j] == chs[n - i - 1]) {
						break;
					}
				}

				count += j - i;

				for (k = j; k > i; k--) {
					chs[k] = chs[k - 1];
				}
				chs[i] = chs[n - i - 1];

			} else {
				for (j = n - i - 1; j >= i; j--) {
					if (chs[j] == chs[i]) {
						break;
					}
				}

				count += n - i - 1 - j;

				for (k = j; k < n - i - 1; k++) {
					chs[k] = chs[k + 1];
				}
				chs[n - i - 1] = chs[i];
			}
		}
		return count;
	}
}