参考:
一、求字典序的第i个排列
参见: http://jay23jack.blog.163.com/blog/static/317951942009130215813/
暴力枚举可以很轻松地得到符合的排列,事实上不妨ms写一个,来产生更强的样例。当然,答案不会是暴力,而是一种很常见的方法,可以说是这种求指定编号问题的通法吧!
直接一位一位枚举答案!从前到后枚举求得每一位:枚举一位时,计算在这样的前缀下,后面的总的排列数。如果严格小于总编号,则该位偏小,换更大的数,同时更新总编号;若大于等于,则该位恰好,枚举下一位,总编号不用更新。
先抛开此题,用最简单的全排列问题来说明这种方法。如1,2,3,4的全排列,共有4!种,求第10个的排列是?
先试首位是1,后234有3!=6种<10,说明1偏小,转化成以2开头的第(10-6=4)个排列,而3!=6 >= 4,说明首位恰是2。
第二位先试1(1没用过),后面2!=2个<4,1偏小,换成3(2用过了)为第二位,总编号也再减去2!,剩下2了。而此时2!>=2,说明第二位恰好是3。
第三位先试1,但后面1!<2,因此改用4。末位则是1了。
这样得出,第10个排列是2-3-4-1。(从1计起)
这种方法的核心在于求,给定前缀下后面总的排列数。全排列问题较易求。同时还需记录前面用过的数字。
再回到此题,同是这样来求。但求后面总的排列数时,用到了递推,或者说dp,不难的方程。
搞懂了后,去poj上做题,先是WA,试过书上的样例后,发现忘记判断“波浪形”了。于是慨叹,强大的数据是成功调试的有力保证!以后还是要不厌其烦地写一些样例来才行啊!
Sam:在求字典序的第i个排列时,关键是求得每种特定前缀子串的可能排列数目,如上面例子中,要求出字典序第i大,需要知道以下序列的个数 (*代表可以使任何可能数字):
1 * * *
1 2 * *
1 2 3 *
1 2 4 *
1 3 * *
1 3 2 *
1 3 4 *
1 4 * *
1 4 2 *
1 4 3 *
2 * * *
2 1 * *
2 1 3 *
2 1 4 *
2 3 * *
2 3 1 *
2 3 4 *
2 4 * *
2 4 1 *
2 4 3 *
二、DP
参见: http://hi.baidu.com/superlong/blog/item/783eb18f9aa063e7f11f36ba.html
这个题目的意思很纠结,是给你一个序列的长度,然后给你一个排列规则,让你输出字典序第m的排列。这个排列规则是这样的:对于排列中的每一个数都有:(a[i]>a[i-1] && a[i]>a[i+1]) || (a[i]<a[i-1] && a[i]<a[i+1]),这个题是用dp加上所谓数学方法(黑书p257),不过另外的dp方程也是可以的,即 DP.down[i][l] 表示以i开始,长度为l,并且初始初始状态是向下放置的(即第二个数小于第一个数),同样DP.up[i][l]就不解释了,那么就有
DP.down[i][l] = sigema{DP.up[[k][l-1](1<=k<i)}
DP.up[i][l] = sigema{DP.down[l+1-i][l].
这样我们可以得到任意状态的排列数,然后就是麻烦的输出了。
-------------------------------------------------------------------------------------------------------------------------------------
代码:
//poj 1037 A decorative fence 经典的DP+计数
#include <iostream>
using namespace std;
int n;
__int64 m;
__int64 dp[21][21][2];
int mark[21];//mark[i]=1表示长度为i的plank已经使用
int main()
{
//1. DP
dp[1][1][0]=dp[1][1][1]=1;
for (int len=2;len<=20;len++){
for (int i=1;i<=len;i++)
{
for (int j=1;j<i;j++) dp[len][i][0]+=dp[len-1][j][1];
for (int j=i;j<len;j++) dp[len][i][1]+=dp[len-1][j][0];//?为什么这里不是n呢???
}
}
//2. 求字典序第i个排列
int t;
scanf("%d",&t);
while (t--)
{
scanf("%d%I64d",&n,&m);
int l=1,r=n,j,finded=0;
memset(mark,0,sizeof(mark));
for (int i=1;i<=n && !finded;i++)
{
for (j=0;j<2;j++)
{
m-=dp[n][i][j];
if (m<=0) {
m+=dp[n][i][j]; //第n位是i,跳出这两个for后需要寻找前缀是1的字典序第m个序列
mark[i]=1;
printf("%d",i);
if (j==0) l=1,r=i-1;
else l=i+1,r=n;
finded=1;
break;
}
}
}
j^=1;
for (int len=n-1;len>=1;len--) //依次找到第(n-1)~1位
{
int num=0;
for (int i=1;i<l;i++) if (!mark[i]) ++num;//本位置剩余可以尝试的数字个数: num???
for (int i=l;i<=r;i++)
if (!mark[i])
{
num++;
m-=dp[len][num][j];
if (m<=0){
m+=dp[len][num][j];
mark[i]=1;
printf(" %d",i);
if (j==0) l=1,r=i-1;
else l=i+1,r=n;
j^=1;
break;
}
}
}
printf("\n");
}
system("pause");
return 0;
}