阿里云高校学生计划,向全国高校学生免费提供ECS云服务器,完成学生认证即可领取,现在领取还可享受免费续费。还有实践训练营、免费AI资源等你来体验,项目经历、组队参赛、考试认证一网打尽。本网站就使用的是阿里云高校计划免费领取的云服务器搭建的哦。
有一天Jerry给Tom出了一道题来考验他。Jerry给了Tom一个长度为2
*n的只包含小写字母的字符串,让Tom将这个字符串任意挑选字符,将其分成两个等长的字符串a和b(对于一个si不能同时被选到a和b中),然后a要和reverse(b)相同(a和反转后的b相同),问这样的方案数有多少?Tom有些为难,所以请你来帮帮他吧。输入一个正整数n,和一个长度为2*n的字符串,输出方案数
输入输出样例如下:
输入:
2
"abba"
输出:
4
题目分析:题目的意思是,有一个长度为2*n的字符串,我们要将其分成两组,每一组放n个字符,至于怎么放是任意的,但是同一个字符不能同时放在两组当中,必须是唯一的。于是就会有很多种放法,如果放在两组中的字符是对称的,比如abbc和cbba,则满足题意,题目求的就是有多少种像这样满足题意的情况。
解题思路:因为字符串中的字符,是等分为两组的,每组可以任意放置字符,因此我们并不需要在意字符串中每个字符的顺序,可以直接统计出这个字符串有多少种字母,每一种字母有多少个,然后进行排列组合就可以了。
因此我们可以选择HashMap或者TreeMap结构来存储每一种字母有多少个,其中key为字符串中出现了的字母,value为该字母对应的出现个数。
综上所述,我选择了HashMap<Character,Integer>来存储字符串中的每一种字母以及其个数。
此时会有两种特殊情况,第一种情况是如果某一种字母的在字符串中为奇数个,则无论如何也不可能放在两组之后,两组的字符串序列是对称的,此时可以直接返回0;第二种情况是,如果字符串中的字符全部为同一个字符,则无论怎么放置,两组字符串序列都是对称的,此时要使用高中的排列组合知识直接计算有多少种情况。
排除以上两种特殊情况后,就剩下一般情况了,需要用到深度优先搜索算法(DFS)来计算有多少种满足题意的情况,由于我个人还没有掌握DFS算法,只知道DFS的大致原理是什么,但是在本题目中以我的能力我讲不清楚此处的DFS算法的原理,还需要继续努力啊。
以上的解题思路,是我在第一期听了学姐和马飞飞大佬讲解后,撰写出来的,没有他们的讲解我可能到现在也毫无头绪,我们要向这些牛人们学习。
package solution106;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Set;
//用于计算组合数的类,来自CSDN
class CombinationNumberCalculation {
public static int[] factor(int n) {
if (n < 0) {
throw new IllegalArgumentException("N must be a non negative integer.");
}
if (n < 4) {
return new int[]{n};
}
int factorNums = (int) (Math.log(Integer.highestOneBit(n)) / Math.log(2));
int[] factors = new int[factorNums];
int factorCount = 0;
for (int i = 2; i <= (int) Math.sqrt(n); i++) {
if (n % i == 0) {
factors[factorCount++] = i;
n /= i;
i = 1;
}
}
factors[factorCount++] = n;
return Arrays.copyOf(factors, factorCount);
}
public static long nchoosek(int n, int k) {
if (n > 70 || (n == 70 && k > 25 && k < 45)) {
throw new IllegalArgumentException("N(" + n + ") and k(" + k + ") don't meet the requirements.");
}
k = k > (n - k) ? n - k : k;
if (k <= 1) { // C(n, 0) = 1, C(n, 1) = n
return k == 0 ? 1 : n;
}
int[] divisors = new int[k]; // n - k + 1 : n
int firstDivisor = n - k + 1;
for (int i = 0; i < k; i++) {
divisors[i] = firstDivisor + i;
}
outer:
for (int dividend = 2; dividend <= k; dividend++) {
for (int i = k - 1; i >= 0; i--) {
int divisor = divisors[i];
if (divisor % dividend == 0) {
divisors[i] = divisor / dividend;
continue outer;
}
}
int[] perms = factor(dividend);
for (int perm : perms) {
for (int j = 0; j < k; j++) {
int divisor = divisors[j];
if (divisor % perm == 0) {
divisors[j] = divisor / perm;
break;
}
}
}
}
long cnk = 1L;
for (int i = 0; i < k; i++) {
cnk *= divisors[i];
}
return cnk;
}
}
public class Solution {
private static StringBuilder groupA = new StringBuilder();//A组
private static StringBuilder groupB = new StringBuilder();//B组
private static long count = 0L;//初始化满足题意的个数
//感谢马飞飞
private static void DFS(StringBuilder builder, int index, HashMap<Character, Integer> map, HashMap<Character, Integer> copiedMap) {
//builder:传入的字符串
//index:角标,用于取出字符
//二叉树:每一个字符是一层
//伪算法如下:
//向字符串a中加入字符
//DFS(left)
//向字符串a中减去字符,向b中加入字符
//DFS(right)
//return
//进一步筛选退出:
//如果a的第一个字符的角标大于n-1,退出
//选定所有字符之后,如果不符合比较的条件,退出
//选完所有字符之后,如果字符串a的大小小于n,退出方法
if (index > builder.length() - 1) {
//来到这一步说明A组字符串第一个角标已经大于n-1
if (groupA.length() < builder.length() / 2) {
return;
}
}
//如果恰好遍历完时有合法序列形成,则不返回,执行下面的代码
if (groupA.length() >= builder.length() / 2) {
//合法序列形成,index以及之后的字符要全部加入到groupB中
int strEnd = groupB.length();
for (int i = index; i < builder.length(); i++) {
char ch = builder.charAt(i);
groupB.append(ch);
}
//此时如果A组和反转后的B组相同,则count加1
if (groupA.toString().equals(groupB.reverse().toString())) {
count++;
}
//将B串反转回来,然后清理B字符串
groupB.reverse();
groupB.delete(strEnd, groupB.length());
return;
}
char ch = builder.charAt(index);//获取当前处理的字符
int countAlphabet = copiedMap.get(ch);
groupA.append(ch);
if (countAlphabet > map.get(ch) / 2) {
//合法字符,向左子树遍历
copiedMap.put(ch, --countAlphabet);//用过之后当前字母对应的value要减去1
DFS(builder, index + 1, map, copiedMap);
} else {
copiedMap.put(ch, --countAlphabet);//用过之后当前字母对应的value要减去1
}
groupA.deleteCharAt(groupA.length() - 1);//删除末尾字符
copiedMap.put(ch, ++countAlphabet);//加回来
//遍历右子树
if (index < builder.length() - 1) {
groupB.append(ch);
DFS(builder, index + 1, map, copiedMap);
groupB.deleteCharAt(groupB.length() - 1);//删除末尾字符
return;
}
//如果指针已经到末尾了,就不需要遍历了,返回
return;
}
public static long solution(int n, String s) {
//创建一个StringBuilder容器
StringBuilder builder = new StringBuilder();
//将传入的字符串s添加到容器中
builder.append(s);
//因为key不能重复,因此用于统计字符串中出现了的字母,value统计这个字母出现的次数
HashMap<Character, Integer> map = new HashMap<>();
//遍历字符串,统计字符串中每一种字母出现的次数
for (int i = 0; i < 2 * n; i++) {
char c = builder.charAt(i);//逐个扫描字符
int countAlphabet = 0;//统计同一个字母出现次数,作为HashMap中key对应的value,初始化为0
//如果map中已经存储了当前扫描到的字母了,就直接把其对应的value赋值给countAlphabet
if (map.containsKey(c)) {
countAlphabet = map.get(c);
}
//将当前正在扫描到的字母添加到map中,由于key不能重复,如果map中已经存在了当前字母,则只增加value
map.put(c, ++countAlphabet);
}
//System.out.println(map);//测试一下统计出来正不正确
//如果出现某个字母只有奇数个,则此题无解,直接返回0
Set<Character> set = map.keySet();//把所有的键放到集合中
for (Character key : set) {
//如果有任何一个键对应的值不是偶数,则肯定无法对称,直接返回0
if (map.get(key) % 2 != 0) {
return 0;
}
}
//如果传入的字符串只有一个字母,则进行排列组合,公式为C(2n,n),上面写好了相关函数
if (1 == map.size()) {
count = CombinationNumberCalculation.nchoosek(2 * n, n);
return count;
}
//拷贝刚刚统计好的集合map到一个新的集合中,用于记录字符的使用情况
HashMap<Character, Integer> copiedMap = new HashMap<>();
copiedMap.putAll(map);//putAll方法将map集合的键值对复制到新的集合
//调用DFS算法
DFS(builder, 0, map, copiedMap);
return count;
}
}
以上代码,自测能过,提交的话超时,哇的一声就哭了!




