Did You AK Today? (今天你AK了吗?)

考虑到本文读者年龄原因,本文改为使用简体中文撰写。

题目描述

今有正整数 n , k n,k n,k,求 1 − n 1-n 1n n n n 个数的全排列,按字典序的第 k k k 个。
数据满足 1 ≤ n ≤ 1 0 5 , 1 ≤ k ≤ min ⁡ ( n ! , 1 0 20   000 ) . 1\leq n\leq10^5,1\leq k\leq\min(n!,10^{20\ 000}). 1n105,1kmin(n!,1020 000).

Solution \text{Solution} Solution

请注意 k k k 的上限! ls忘了打。这很重要!

对于 50 % 50\% 50% 及更低的数据,ls已经解释得很详尽,在此无需赘述。

尝试使用 DeCantor Expansion(逆康托展开)算法求解。这篇文章有详细解释,看不懂找 ls 他英语一级棒。时间复杂度 O ( n 2 ) O(n^2) O(n2)
预处理 1 − n 1-n 1n 的阶乘,时间复杂度 O ( n 2 ) O(n^2) O(n2)
朴素的高精度除法,时间复杂度 O ( n 2 ) O(n^2) O(n2)

综上所述,总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。贴上代码。

#include<cstdio>
#include<cstdlib>
#include<cstring>

#define reg register
typedef long long ll;

const int MAXN=1010;	//最长 1000位
const int BYTE=8;		//压8位,也就是说一个int代表8位数
const int MB=MAXN/BYTE+1;	//新的长度

int pw[BYTE+10];	//pw[i]表示10^i
char str[MAXN+10];


struct Bignum{		//高精度类
	int a[MB+10];
	Bignum(){
		memset(a,0,sizeof(a));
	}
	void read(){	//读入、赋值、压位
		scanf("%s",str+1);
		int len=strlen(str+1);
		for(reg int i=1;i<=len;++i)
			a[i]=str[len-i+1]-'0';
		for(reg int i=1;i<=len/BYTE+1;++i){
			int cnt=0;
			for(reg int j=i*BYTE;j>=(i-1)*BYTE+1;--j)
				cnt=cnt*10+a[j];
			a[i]=cnt;
		}
		//注意清零
		for(reg int i=len/BYTE+2;i<=len;++i) a[i]=0;
	}
	void print(){	//打印一个数
		bool tf=0;
		for(reg int i=MB+1;i>=1;--i){
			if(a[i]&&!tf){
				tf=1;
				printf("%d",a[i]);
				continue;
			}
			if(tf) printf("%08d",a[i]);
		}
		if(!tf) putchar('0');
	}
	//重定义Bignum类的运算,高精+高精
	friend Bignum operator+(const Bignum a,const Bignum b){
		Bignum c;
		for(reg int i=1;i<=MB+1;++i)
			c.a[i]=a.a[i]+b.a[i];
		for(reg int i=1;i<=MB+1;++i)
			if(c.a[i]>=pw[BYTE]){
				c.a[i+1]+=c.a[i]/pw[BYTE];
				c.a[i]%=pw[BYTE];
			}
		return c;
	}
	//高精减
	friend Bignum operator-(const Bignum a,const Bignum b){
		Bignum c;
		for(reg int i=1;i<=MB+1;++i)
			c.a[i]=a.a[i]-b.a[i];
		for(reg int i=1;i<=MB+1;++i)
			if(c.a[i]<0){
				c.a[i]+=10;
				--c.a[i+1];
			}
		return c;
	}
	//高精乘低精
	friend Bignum operator*(const Bignum a,const int b){
		Bignum c;
		ll d[MB+10];
		for(reg int i=1;i<=MB+1;++i)
			d[i]=(ll)a.a[i]*b;
		for(reg int i=1;i<=MB+1;++i){
			if(d[i]>=pw[BYTE]){
				d[i+1]+=d[i]/pw[BYTE];
				d[i]%=pw[BYTE];
			}
			c.a[i]=(int)d[i];
		}
		return c;
	}
	//注意此处的>等价于>=
	friend bool operator>(const Bignum a,const Bignum b){
		for(reg int i=MB+1;i>=1;--i)
			if(a.a[i]!=b.a[i])
				return a.a[i]>b.a[i];
		return 1;
	}
}ft[MB+10],k,one;
//ft[i]表示i的阶乘
struct Pair{	//两个大数
	Bignum a,b;
};
int n;
bool tf[MAXN+10];	//tf[i]表示第i个数是否可以被取用
int ans[MAXN+10];	//ans是答案数组

//朴素高精除,返回的a是商,b是余数
Pair operator/(const Bignum a,const Bignum b){
	Pair c;Bignum d;
	for(reg int i=MB+1;i>=1;--i){
		d=d*10;d.a[1]=a.a[i];
		c.a=c.a*10;
		while(d>b){
			d=d-b;
			++c.a.a[1];
		}
	}
	c.b=d;
	return c;
}
void reset(){	//初始化
	pw[0]=1;
	for(reg int i=1;i<=BYTE;++i)
		pw[i]=pw[i-1]*10;
	ft[1].a[1]=1;
	for(reg int i=2;i<=n;++i)
		ft[i]=ft[i-1]*i;
	memset(tf,1,sizeof(tf));
	one.a[1]=1;
	k=k-one;
}
int cg(const Bignum a){	//将一个Bignum转换成int
	int cnt=0;
	for(reg int i=MB+1;i>=1;--i)
		cnt=cnt*pw[BYTE]+a.a[i];
	return cnt;
}
int find(int x){	//找到能使用的第x小的数
	int cnt=0;
	for(reg int i=1;i<=n;++i)
		if(tf[i]){
			++cnt;
			if(cnt==x){
				tf[i]=0;	//取用它
				return i;
			}
		}
	return -1;
}
void work(){	//DeCantor Expansion
	Pair p;
	int cnt;
	//此处参考算法解释的文章
	for(reg int i=1;i<n;++i){
		p=k/ft[n-i];k=p.b;
		cnt=cg(p.a);
		ans[i]=find(cnt+1);
	}
	ans[n]=find(1);	//最后只剩一个数了,取用它
}
int main(){
	scanf("%d",&n);k.read();
	reset();
	work();
	for(reg int i=1;i<=n;++i)
		printf("%d ",ans[i]);
}

最后一个点的 100   000 100\ 000 100 000,看上去很吓人,但是由于 k ≤ 1 0 20   000 k\leq10^{20\ 000} k1020 000,所以前 96   000 96\ 000 96 000 位左右都是原来的顺序(本质上还是 n 2 n^2 n2 的)。这个点的特判我不管了你自己写。

优化方法

  1. 使用树状数组维护一个数是否被取用。时间复杂度 O ( n   log ⁡   n ) O(n\ \log\ n) O(n log n)
  2. 快速多项式除法,或者二分答案。时间复杂度 O ( n   log ⁡   n ) O(n\ \log\ n) O(n log n)
  3. 使用压位存储。若压 B Y T E BYTE BYTE 位,则时间复杂度 O ( O ( 原 来 ) B Y T E 2 ) O(\frac{O(原来)}{BYTE^2}) O(BYTE2O())
  4. 使用任意模数快速阶乘。时间复杂度 O ( n   log ⁡   n ) O(n\ \log\ n) O(n log n)(未实践)。

期望时间复杂度 O ( n   log ⁡   n ) O(n\ \log\ n) O(n log n)。若哪位巨佬实现了,请评论并收下我的膝盖orz。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值