- 原题链接:Here!
- 一开始想直接用递归做,因为以前做过采用递归求全排列的问题,但是这样会走进一个误区,这个题目要求按字典序排,所以不能按照全排列的思想去解决。
字典序这个要求比较有意思,从字典序中可以找到一些规律,用这些规律来解决掉这个问题。
- 假设拿 n=3 m=10来说
它的子集按照字典序排列会有如下顺序:{1} {1,2} {1,2,3} {1,3} {1,3,2} {2} {2,1} {2,1,3} {2,3} {2,3,1} {3} {3,1} {3,1,2} {3,2} {3,2,1}
从子集顺序上可以发现它们能够分成3组,每一组都有一个开头数字,去掉开头数字就是n=2时的情况数+1(空集)
所以
1.设f(n)是n个数字按照字典序所产生的子集个数,f(n) = n*( f(n-1) + 1 ),f(1)=1
这里需要强调按照字典序生成的子集,一个含有n个元素的集合真子集的个数是2^n-1,为什么按照字典序生成的子集却不符合这一规律?因为在按字典序生成时{1,2}和{2,1}认为是两个不同的集合,所以 f(n) >> 2^n-1。
2.设g(n)是每一组子集的个数,g(n)=f(n)/n
g(n-1)=f(n-1)/(n-1),f(n) = n*( f(n-1) + 1 ),g(n)=(n-1)*g(n-1)+1
从上面子集顺序可以得到一个思路,我们可以先输出开头数字,然后把问题规模缩小到( n-1 , m-(t-1)*g(n)-1 ),不断缩小规模直至找到答案。
怎么得到的 m-(t-1)*g(n)-1 ? t代表所求子集所在的组,每次问题规模缩小时,m都需要减去多余的子集,多余的子集数就是上面1~t-1组所含子集数和t组去掉开头数字后剩余的空集。
主要步骤:
1、求出所在组t
2、输出所在组t的首元素s[t](同一组首元素相同)
3、将该子集的下一个元素到最后一个的值+1,注意这个规律:在第i组,首元素为i,删除首元素后,在第i个子集后首元素均变大+1.
4、缩减问题规模继续执行步骤1~3
-
CODE:
#include<cstdio> using namespace std; #define LL long long // 因为上面分析f(n)<<2^n-1 因此g(n)和m用longlong //#define test int n; // n:一共多少元素<=20。t:所求子集位于分组后的第几组 LL m; // m:位于某一组的第几个子集 int s[21]; // 后面将子集按字典序分组后每组的初始首元素,组数<=20 LL g[21]={0}; // 后面将子集分组后平均每组个数,如:c[2]表示n=2时的分组每组中子集数 void set_table(){ // 先打个表 for (i=1;i<21;i++) g[i]=g[i-1]*(i-1)+1; // 推导出来的c[n]=(n-1)*c[n-1]+1 } int main(){ #ifdef test freopen("Hdu 2062 Subset sequence.txt","r",stdin); #endif set_table(); while (scanf("%d%lld",&n,&m)!=EOF){ for(i=0;i<21;i++) s[i]=i; // 每循环一次就重新归位每组首元素 while (n>0&&m>0){ int t=m/g[n]+(m%g[n]>0?1:0); // 得到第m个子集在分组后的第t组 if(t>0){ printf("%d",s[t]); for(i=t;i<=n;i++) s[i]=s[i+1]; // 当去掉开头数字后,大于开头数字的数+1 m-=((t-1)*g[n]+1); // 减去(t-1组总子集数+1),m变为表示在剩余子集中位于第几个 putchar(m==0?'\n':' '); } n--; // 依次递减,直到执行上面的if代码或退出 } } return 0; }