uva 11027 Palindromic Permutation

数学题(字符串的解码与编码,涉及组合数学)

题意:给你一个字符串,它们的全排列中有一些字符将会是回文串,单独把这个些回文串拿出来,按字典序给他们从1开始编号。然后输入数字n,把第n个回文串输出。

这题第一次看完全不会放下几天,今天再看瞬间想通。首先这题要从回文串的性质分析:一个回文串如果长度为偶数,那么可以确定,每种字符的个数一个是偶数,不会有字符的个数为奇数。如果一个回文串为奇数,那么可以确定,一定有且仅有一种字符的个数为奇数,其他字符的个数都会偶数,而且整个回文串中间的那个字符必定是为奇数的那个字符。所以对于长度为奇数的回文串,我们可以暂时除掉中间的那个字符(因为它是固定一定要在那里,没什么研究价值),把它变为一个长度为偶数的回文串(但是记得最终输出的时候把除掉的那个字符补上)。我们来看长度为偶数的回文串,两两对称,所有我们只需要研究左半部分,右半部分只是复制罢了,也就是说,一个回文串最终长什么样,是由左半部分决定的。

所以我们只对左半部分按照字典序排序,那么左半部分的排序结果也会是整个回文串的排序结果

有人不禁要质疑上面的这条结论,说是会不会左半部分的排序和整个回文串的排序不同,这是不可能的,因为字典序的比较是第一次遇到不同就停止。

所以我们要找第n个回文串,实际就是找左半字符串全排列中的第n个排列,这是一个解码的过程,就是一位一位地确定字符

我们用例子来说明问题:

假设经过处理后我们的到的左半部分字符串是ababcdac,我们要知道第698个排列的字符串

3个a,2个b,2个c,1个d,总长度为8

我们从第一位开始确定,第一位可以是a,b,c,d

第一位为a,剩下2个a,2个b,2个c,1个d,他们的全排列个数要用组合数学来算为 (2+2+2+1)! / (2!2!2!1!)=630

而698>630,所以第1位不能填a , 698-630=68

第一位为b,剩下3个a,1个b,2个c,1个d,全排序个数为(3+1+2+1)! / (3!1!2!1!)=420

而68<420 , 说明第一位填b

——————————————————————————

68 , 3个a,1个b,2个c,1个d,总长度为7

若第二位填a,剩下2个a,1个b,2个c,1个d,全排列数为 6!/ 2!1!2!1! = 180 , 68<180 , 说明第2位填a

——————————————————————————

68 , 2个a,1个b,2个c,1个d , 总长度为6

若第3位填a,剩下1个a,1个b,2个c,1个d, 全排列数为 5!/ 1!1!2!1! = 60 , 68>60 , 不能填a , 68-60=8

若第3位填b,剩下2个a,2个c,1个d,全排列为 5!/ 2!2!1! = 30 , 8<30 , 说明第3位填b

———————————————————————————

8 , 2个a,2个c,1个d , 总长度为5

若第4位填a,剩下1个a,2个c,1个d  , 全排列个数为 4!/ 1!2!1! = 12 , 若8<12 , 说明第4位填a

———————————————————————————

8,1个a,2个c,1个d , 总长度为4

若第5位填a,剩下2个c,1个d,全排列数为  3!/ 2!1!=3 , 8>3 , 不能填a,8-3=5

若第5位填c,剩下1个a,1个c,1个d,全排列为 3! / 1!1!1! = 6 , 5<6 , 说明第5位填c

————————————————————————————

5 ,  1个a,1个c,1个d , 总长度为3

若第6位为a , 剩下 1个c,1个d , 全排列为 2 , 5>2 , 不能填a , 5-2=3

若第6位为c,  剩下1个a,1个d, 全排列为 2 , 3>2 , 不能填c , 3-2=1

若第6位为d,  剩下1个a,1个c,全排列为2 , 1<2 , 说明第6位为d

—————————————————————————————

1 , 1个a,1个c , 总长度为2

若第7位为a , 剩下1个c, 全排列数为 1 , 1<=1 , 说明第7位填a

——————————————————————————————

1 , 1个c , 第8位填c

——————————————————————————————

最终得到字符串为  babacdac

上面的迭代过程用代码实现即可,不过注意一个陷阱,输入中的字符串可能本身根本无法构成回文(超过一种字符的个数为奇数)

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

long long num;
int LEN;  
int c[30];
char ans[30],mid;

long long P(int n)
{
    long long ans=1;
    while(n)  { ans*=n; n--;}
    return ans;
}
void solve()
{
    long long a=1,b=1;
    a=P(LEN);
    for(int i=0; i<26; i++) if(c[i]) b*=P(c[i]);
    if(num>a/b) 
    { printf("XXX\n"); return ;}   //超出总排列数

    int i,j,cur=1;
    memset(ans,0,sizeof(ans));
    while(cur<=LEN)
    {
        for(i=0; i<26; i++) if(c[i])//枚举当前位能填的字母
        {
            c[i]--;  //暂时减1,到时候看情况加回来
            a=P(LEN-cur);  //分子
            for(b=1,j=0; j<26; j++) if(c[j])
                b*=P(c[j]);  //分母
            long long m=a/b;  //填i这个字母的排列数
            if(num>m)  { num-=m; c[i]++; }  //填i不够,需要枚举下一个
            else       { ans[cur]=i; break; } //当天位填i
        }
        cur++;
    }
    for(i=1; i<=LEN; i++) printf("%c",ans[i]+'a');
    if(mid) printf("%c",mid);
    for(i=LEN; i>=1; i--) printf("%c",ans[i]+'a');
    printf("\n");
}

bool init()
{
    int i,ccount;
    char s[50];
    scanf("%s%lld",s,&num);  LEN=strlen(s);

    memset(c,0,sizeof(c));
    for(int i=0; i<LEN; i++) c[s[i]-'a']++;
    
    
    for(mid='\0' , ccount=0 , i=0; i<26; i++)  
    {
        if(c[i]&1) //扫描26个字母,找出个数为奇数的作为中间值
        { mid=i+'a';  ccount++; } //统计多少个字符的个数为奇数
        c[i]=c[i]>>1;
        //个数都减半,因为我们只要一半去构建左半部分
    }
    LEN=LEN>>1;  //总长度也折半
    if(ccount>1)   return 0;  //原字符串不可能构成回文
    else           return 1;
}

int main()
{
    int Case,T;
    scanf("%d",&T);
    for(Case=1; Case<=T; Case++)
    {
        bool a=init();
        printf("Case %d: ",Case);
        if(!a)  printf("XXX\n");  //原字符串本身不能构成回文
        else    solve();
    }
    return 0;
}

最后复习一下。有m种小球,每种小球的个数分别为n1,n2,n3……nm,总的排列总数为

(n1+n2+n3……nm)! / (n1!n2!n3!……nm!)

原文地址:https://www.cnblogs.com/scau20110726/p/2891543.html