LOJ3228 Tree Depth 和 LOJ6077 逆序对 和 LOJ6089 小 Y 的背包计数问题

Tree Depth

考虑所有长度为\(N\)有恰好\(K\)个逆序对的排列。

对于每个位置\(i\),求出其在所有满足条件的排列中,在笛卡尔树上的深度和。

对读入的质数\(\text{MOD}\)取模。

\(N ≤ 300,K ≤\binom{N}{2}, \text{MOD} ∈ [10^8, 10^9 + 9]\)

题解

题目对排列的逆序对个数有要求,考虑能顺便计算逆序对个数的构造排列的两种过程:

  1. 按照权值从小到大构造。构造出\(1\sim x-1\)的相对位置后,决定插入\(x\)的相对位置。新增的逆序对个数\(\in [0,x-1]\)

  2. 按照位置从前往后构造。构造出\(P_1\sim P_{x-1}\)的相对大小后,决定赋值给\(P_x\)的相对大小。新增的逆序对个数\(\in [0,x-1]\)

回到这道题。题目要求笛卡尔树上每个节点的深度和,利用期望的线性性转化为\(j\)\(i\)的祖先的方案数。

考虑\(j\)\(i\)的祖先需要满足的条件,\(\min_{\min\{i,j\}\leq k\leq \max\{i,j\}}\{P_k\}=P_j\)。显然第二种构造方法更有利于这种形式的计算。

注意到第二种方法能执行的条件只需要当前填放的位置是连续的,所以我们可以改变一下填放顺序。

考虑把插入的过程分成两部分:

  1. \(i\)开始向\(j\)的方向,决定每个元素在当前排列中的相对大小。

  2. \(i\)开始向\(j\)的反方向,决定每个元素在当前排列中的相对大小。

这样除了需要成为\(i\)祖先的\(j\)的区间缩成一个单点之外,每次新产生的逆序对数目区间一定是\([0,1] , [0,2] , ⋯ ,[0,N − 1]\)这样。

  • \(j < i\)时,\(j\)必定产生\(0\)个逆序对。

  • \(j >i\)时,\(j\)必定产生\(j − i\)个逆序对。

先假装下标为\(i\)的每次产生逆序对个数在\([0, i − 1]\)内,做一次\(DP\)。每次枚举\(|j-i|\),撤销掉其本来的贡献;然后枚举\(i\)计算其对\(i\)答案的贡献。

注意到这个DP过程的本质是背包,所以可以用多项式计算。用\(1+x+x^2+\dots+x^i=\frac{x^{i+1}-1}{x-1}\)计算来保证复杂度。

时间复杂度\(O(N^3)\)

CO int N=301;
int ans[N];

int main(){
	int n=read<int>(),K=read<int>();
	read(mod);
	poly F={1};
	for(int d=1;d<n;++d){
		F.resize(F.size()+(d+1));
		for(int i=(int)F.size()-1;i>=0;--i)
			F[i]=add(i-(d+1)>=0?F[i-(d+1)]:0,mod-F[i]);
		for(int i=0;i<=(int)F.size()-1-1;++i)
			F[i]=add(mod-F[i],i-1>=0?F[i-1]:0);
		F.resize(F.size()-1);
	}
	fill(ans+1,ans+n+1,F[K]);
	for(int d=1;d<n;++d){
		poly G=F;G.resize(G.size()+1);
		for(int i=(int)G.size()-1;i>=0;--i)
			G[i]=add(i-1>=0?G[i-1]:0,mod-G[i]);
		for(int i=0;i<=(int)G.size()-1-(d+1);++i)
			G[i]=add(mod-G[i],i-(d+1)>=0?G[i-(d+1)]:0);
		G.resize(G.size()-(d+1));
		for(int i=1;i<=n;++i){
			if(i-d>=1)
				ans[i]=add(ans[i],0<=K and K<(int)G.size()?G[K]:0);
			if(i+d<=n) 
				ans[i]=add(ans[i],0<=K-d and K-d<(int)G.size()?G[K-d]:0);
		}
	}
	for(int i=1;i<=n;++i) printf("%d%c",ans[i]," \n"[i==n]);
	return 0;
}

暴力实现多项式乘法的时候,意义十分明显。

暴力实现多项式除法的时候,如果保证了能够整除,那么从低位到高位考虑意义比较好想。

逆序对

给定 \(n, k\),请求出长度为 \(n\) 的逆序对数恰好为 \(k\) 的排列的个数。答案对 \(10 ^ 9 + 7\) 取模。

对于 \(100\%\) 的数据,\(1 \leq n, k \leq 100000, 1 \leq k \leq \binom{n}{2}\)

题解

https://ouuan.github.io/post/loj6077-逆序对生成函数计数dp/

从小到大依次考虑将每个数插入排列,那么每个数 \(i\) 都可以贡献 \(0\dots i-1\) 个逆序对,所以答案的生成函数为 \((1 + x)(1 + x + x^2)\cdots(1+x+\cdots+x^{n-1})\)

上下同时乘上 \((1-x)^n\),即求:

\[\frac{(1-x)(1-x^2)\cdots(1-x^n)}{(1-x)^n} \]

(不约分是为了方便求。)

分母 \(\frac{1}{(1-x)^n}=\sum\limits_{i\ge 0}\binom{n-1+i}{n-1}x^i\),是一个大家熟知的结论,可以利用 \((1+x+x^2+\cdots)^n\) 的组合意义说明。

方程\(x_1+x_2+\dots+x_n=i\)的非负整数解个数。也就是组合数隔板法。

分子的 \(x^i\) 项系数的组合意义为:考虑从 \(1,2,\ldots,n\) 中选若干个和为 \(i\) 的数(每个数只能选一遍)的所有方案,若选了奇数个数贡献为 \(-1\),若选了偶数个数贡献为 \(1\)

\(f_{i,j}\) 表示选 \(i\) 个数和为 \(j\) 的方案数。

由于选择的数两两不同,第一维的大小是 \(O(\sqrt k)\) 的。

转移有两种方式:

  1. 背包里的所有数加一,\(f_{i,j}\gets f_{i,j-i}\)

  2. 背包里的所有数加一,并向背包中放入一个体积为 \(1\) 的物品,\(f_{i,j}\gets f_{i-1,j-(i-1)-1}\)

但这样算可能会出现体积大于 \(n\) 的物品。具体来说,当 \(j\ge n+1\) 时,会有 \(f_{i-1,j-n-1}\) 种不合法的方案,需要减去。

计算完 dp 之后,分子的 \(x^i\) 项系数即为 \(\sum\limits_{j\ge0}(-1)^jf_{j,i}\)

最后把分子和分母卷积起来即可,总时间复杂度为 \(O(n+k\sqrt k)\)\(O(n\log p+k\sqrt k)\)(取决于计算组合数与逆元的方式)。

CO int N=1e5+10;
int fac[2*N],ifac[2*N];
int f[2][N];

IN int C(int n,int m){
	return mul(fac[n],mul(ifac[m],ifac[n-m]));
}
int main(){
	int n=read<int>(),m=read<int>();
	fac[0]=1;
	for(int i=1;i<=n+m;++i) fac[i]=mul(fac[i-1],i);
	ifac[n+m]=fpow(fac[n+m],mod-2);
	for(int i=n+m-1;i>=0;--i) ifac[i]=mul(ifac[i+1],i+1);
	int ans=C(n-1+m,m),o=0;
	f[o][0]=1;
	for(int i=1,sum=1;sum<=m;++i,sum+=i){ // at least sum
		o^=1,memset(f[o],0,sizeof(int)*sum);
		for(int j=sum;j<=m;++j){
			f[o][j]=add(f[o^1][j-i],f[o][j-i]);
			if(j>=n+1) f[o][j]=add(f[o][j],mod-f[o^1][j-n-1]);
			ans=add(ans,mul(mul(i&1?mod-1:1,f[o][j]),C(n-1+m-j,m-j)));
		}
	}
	write(ans,'\n');
	return 0;
}

https://www.cnblogs.com/yinwuxiao/p/9741568.html

讲道理\(\prod_{i=1}^n(1-x^i)\)这么好看,应该可以快速求。

类似整数划分计数取个\(\ln\),奇妙的事情发生了。

\[\ln\prod_{i=1}^n(1-x^i)=\sum_{i=1}^n\ln(1-x^i)\\ =\sum_{i=1}^n\sum_{j=1}^{\infty}-\frac{1}{j}(x^j)^i\\ =\sum_{i=1}^nx^i\sum_{j|i}-\frac{1}{j} \]

  1. \(-\ln(1-x)=x+\frac{1}{2}x^2+\frac{1}{3}x^3+\dots\)

  2. \(-\ln(1-x^2)=x^2+\frac{1}{2}x^4+\frac{1}{3}x^6+\dots\)

观察这两行应该就能找出规律。

做个乘法卷积(狄利克雷)卷积然后\(\exp\)回去就好了。

时间复杂度\(O(n\log n)\),与\(k\)无关,相当优秀。

小 Y 的背包计数问题

小 Y 有一个大小为 \(n\) 的背包,并且小 \(Y\)\(n\) 种物品。

对于第 \(i\) 种物品,共有 \(i\) 个可以使用,并且对于每一个 \(i\) 物品,体积均为 \(i\)

求小 \(Y\) 把该背包装满的方案数为多少,答案对于 \(23333333\) 取模。

定义两种不同的方案为:当且仅当至少存在一种物品的使用数量不同。

对于 \(100\%\) 的数据,满足 \(n \le 10^5\)

题解

https://ouuan.github.io/post/loj6089-小y的背包计数问题根号分治计数dp/

简要作法

体积大于等于 \(\sqrt n\) 的物品可以无限选,所以考虑分开处理小于根号的和大于等于根号的。

小于根号的

\(f_{i, j}\) 表示从前 \(i\) 种物品中选体积为 \(j\) 的方案数。

\[f_{i, j} = \sum\limits_{k = 0}^{\min(i, \left\lfloor\frac j i\right\rfloor)}f_{i-1, j - ik} \]

可以使用模 \(i\) 同余的前缀和优化。

这部分的时间复杂度为 \(O(n\sqrt n)\),空间复杂度可以优化至 \(O(n)\)

大于等于根号的

\(g_{i, j}\) 表示选择 \(i\) 个物品体积为 \(j\) 的方案数。

转移有两种方式:

  1. 向背包中放入一个体积为 \(\left\lceil\sqrt n\right\rceil\) 的物品。

  2. 将背包中所有物品体积加一。

\[g_{i, j} = g_{i - 1, j - \left\lceil\sqrt n\right\rceil} + g_{i, j - i} \]

由于最多选 \(\left\lfloor\sqrt n\right\rfloor\) 个物品,第一维大小为 \(O(\sqrt n)\),这部分复杂度也是 \(O(n\sqrt n)\)

合并

相当于求卷积的一位。

两部分加起来体积为 \(n\) 就计入答案。

需要注意的是,第二部分中体积为 \(k\) 的方案数是 \(\sum\limits_{i=0}^{\left\lfloor\sqrt n\right\rfloor}g_{i, k}\),而不是某个单独的 dp 值。

CO int N=1e5+10;
int f[N],pre[N],g[2][N];

int main(){
	int n=read<int>(),b=sqrt(n)+1;
	f[0]=1;
	for(int i=1;i<b;++i){
		for(int j=0;j<=n;++j) pre[j]=add(f[j],j>=i?pre[j-i]:0);
		for(int j=0;j<=n;++j){
			if(j<i*(i+1)) f[j]=pre[j];
			else f[j]=add(pre[j],mod-pre[j-i*(i+1)]);
		}
	}
	int ans=f[n],o=0;
	g[o][0]=1;
	for(int i=1;i<b;++i){
		o^=1,memset(g[o],0,sizeof(int)*i*b);
		for(int j=i*b;j<=n;++j){
			g[o][j]=add(g[o^1][j-b],g[o][j-i]);
			ans=add(ans,mul(f[n-j],g[o][j]));
		}
	}
	write(ans,'\n');
	return 0;
}

posted on 2020-08-05 19:40  autoint  阅读(150)  评论(0编辑  收藏

导航