蓝桥杯 2018-10 堆的计数

我们知道包含N个元素的堆可以看成是一棵包含N个节点的完全二叉树。 每个节点有一个权值。对于小根堆来说,父节点的权值一定小于其子节点的权值。 假设N个节点的权值分别是1~N,你能求出一共有多少种不同的小根堆吗?由于数量可能超过整型范围,你只需要输出结果除以1000000009的余数。

输入
一个整数N。
对于40%的数据,1<=N<=1000
对于70%的数据,1<=N<=10000
对于100%的数据,1<=N<=100000
输出
一个整数表示答案。
样例输入复制
4
样例输出复制
3
一、埃氏筛求规定范围内的素数的个数
每找到一个素数,就把后面的数是该素数倍数的数减去

二、快速幂
求a的n次方,把n化成二进制,比如313

代码:(因为求幂可能值很大,所以一般会取余)

public class 快速幂 {
static int mod = 1e9;
public static void main(String[] args) {
	System.out.println(f(3, 13));
}
private static int f(int a, int b) {
	// TODO Auto-generated method stub
	int res = 1;
	int x = a;
	while (b > 0) {
		if ((b & 1) == 1)//寻找二进制中的1
			res = res * x % mod;
		x = x * x % mod;//相当于,比如二进制的第一位是1,表示x,第二位是1,表示x*x,第三位是1,表示x*x*x*x,这样累乘
		//找到一个1后,x恰好是该位为1时,对应的值
		b = b / 2;//找下一位
	}
	return res;
}

}

三、逆元
参考:链接
(a / b)mod p = (a * c) mod p,则c时b的逆元,根据等式,可以简单理解为在取模运算中,a / b = a * c ,所以c = 1 / b.

结论:要使得(a / b)mod p = (ac) mod p,那么就需要bc 与 1模p同余,即 ( b*c)mod p = 1 mod p;

结论:(a / b)mod p = (a*bp - 2) mod p
即b的逆元是bp - 2
根据逆元求组合数的值

上面代码比较好理解,第一个for循环求n的阶乘
第二个for循环求从2到k的逆元,然后相乘
第三个for循环求从2到n - k的逆元,然后相乘
第二个和第三个for循环其实就是将复杂的阶乘的逆元,转换成简单的逆元,其中用到了快速幂,其实个人认为也不需要证明为什么可以这样做,因为,你把阶乘直接展开,不就是求每一个因子的逆元嘛。
四、卢卡斯定理
五、这道题
首先堆是一个完全二叉树的结构,那么就要想到树结构的计算特性,就是递归,因为每一个小二叉树结构是一样的,而树结构用到最多的就是递归,但是递归其实可以转化为记忆型递归,用一个数组保存结果,其实也可以说是dp。
dp[i]:表示以i为根节点的可以形成的堆的个数n,对于根节点i的左子树其节点个数为l,那么C(l,n - 1)表示的是从剩余的节点里面选出l个给左子树,左子树有了,剩下的自然就给右子树了,所以不用再计算右子树。
转移方程:dp[i] = C(l,n - 1)dp[i2]dp[i2+1];
这个方程不太好理解,但这个方程也是这道题的关键。
dp[i2]dp[i2+1]表示,以i为根节点的堆的个数等于左子堆的个数乘以右子堆的个数。这个相对好理解一点,比如左子堆有2种排列形式,右子堆有3种排列形式,则二者一共有23种排列形式
但是C(l,n - 1)呢?表示左子树中实际的值的种数。
综合起来理解,可以是,先选出左子树实际的值C(l,n - 1),然后对这些值进行排列dp[i*2]。

import java.util.Scanner;
public class jt10 {
	static int mod = 1000000009;
	static long[] f;
	static long[] inv;

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int[] s = new int[n + 1];// 求以i为根节点的子树,包括i的结点的个数。
		f = new long[n + 1];// i的阶乘
		long[] dp = new long[n + 1];// 以i为根节点的堆的种数
		inv = new long[n + 1];// n!的逆元
		for (int i = n; i >= 1; i--) {
			s[i]++;
			if (i * 2 <= n)
				s[i] += s[i * 2];
			if (i * 2 + 1 <= n)
				s[i] += s[i * 2 + 1];
		}
		f[0] = 1;
		inv[0] = 1;
		for (int i = 1; i <= n; i++) {
			f[i] = f[i - 1] * i % mod;
			inv[i] = pow(f[i], mod - 2);
			// System.out.println(f[i] + " " + inv[i] + " $$");
		}
		for (int i = 0; i < dp.length; i++) {
			dp[i] = 1;
		}
		for (int i = n; i > 0; i--) {
			if (2 * i + 1 <= n) {
				// System.out.println(s[i * 2] + " " + (s[i] - 1) + " ((" + i * 2 + " " + i);
				dp[i] = c(s[i * 2], s[i] - 1) * dp[i * 2] % mod * dp[i * 2 + 1] % mod;
			} else if (2 * i <= n) {
				dp[i] = c(s[i * 2], s[i] - 1) * dp[i * 2] % mod;
			}

		}
		System.out.println(dp[1]);
	}

	private static long c(int k, int n) {
		return f[n] * inv[k] % mod * inv[n - k] % mod;
	}

	private static long pow(Long i, int j) {
		// TODO Auto-generated method stub
		if (i == 0)
			return 0;
		long res = 1;
		long x = i;
		while (j > 0) {
			if ((j & 1) == 1) {
				res = res * x % mod;
			}
			x = x * x % mod;
			j = j / 2;
		}
		return res;
	}
}

调错调了一个小时,我去!!!!!!!!!!!
最后把正确代码一点点取代我的代码,在最后取代的一个代码块,才发现我的错误!!!!!!!!!!
错误一:
快速幂,因为求幂的结果会很大,所以要取余,既然结果很大,那么res和x都应该是长整型。!!!!!!
错误二:
求逆元,inv[i]表示的是i的阶乘的逆元,不是i的!!!!!!!!!!这里是直接求阶乘的逆元,而不是求阶乘的因子的逆元再相乘。如果这样的话,也可以代码就变为

import java.util.Scanner;

public class jt10 {
	static int mod = 1000000009;
	static long[] f;
	static long[] inv;

	public static void main(String[] args) {
		Scanner scanner = new Scanner(System.in);
		int n = scanner.nextInt();
		int[] s = new int[n + 1];// 求以i为根节点的子树,包括i的结点的个数。
		f = new long[n + 1];// i的阶乘
		long[] dp = new long[n + 1];// 以i为根节点的堆的种数
		inv = new long[n + 1];// n!的逆元
		for (int i = n; i >= 1; i--) {
			s[i]++;
			if (i * 2 <= n)
				s[i] += s[i * 2];
			if (i * 2 + 1 <= n)
				s[i] += s[i * 2 + 1];
		}
		f[0] = 1;
		inv[0] = 1;
		for (int i = 1; i <= n; i++) {
			f[i] = f[i - 1] * i % mod;
			// inv[i] = pow(f[i], mod - 2);
			inv[i] = pow(i, mod - 2) * inv[i - 1] % mod;
		}
		for (int i = 0; i < dp.length; i++) {
			dp[i] = 1;
		}
		for (int i = n; i > 0; i--) {
			if (2 * i + 1 <= n) {
				dp[i] = c(s[i * 2], s[i] - 1) * dp[i * 2] % mod * dp[i * 2 + 1] % mod;
			} else if (2 * i <= n) {
				dp[i] = c(s[i * 2], s[i] - 1) * dp[i * 2] % mod;
			}

		}
		System.out.println(dp[1]);
	}

	private static long c(int k, int n) {
		return f[n] * inv[k] % mod * inv[n - k] % mod;
	}

	private static long pow(int i, int j) {
		if (i == 0)
			return 0;
		long res = 1;
		long x = i;
		while (j > 0) {
			if ((j & 1) == 1) {
				res = res * x % mod;
			}
			x = x * x % mod;
			j = j / 2;
		}
		return res;
	}
}

注意是哪里变了,

// inv[i] = pow(f[i], mod - 2);
inv[i] = pow(i, mod - 2) * inv[i - 1] % mod;

并且把pow函数的第一个参数改为长整型,其实这样利用了之前的逆元,比直接求要快一些。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值