挑战编程-第六章-组合数学-总结
学习用java处理大数很有必要,解组合数学的题就是一个找公式推规律的过程。推公式的过程
又类似于推导状态转移方程,再推出公式后,往往会发现题目的数据是超long long的,再用大数
方法来处理,代码很随意就能上200行,但是如果用java来写的话,推出公式,组合数学的题就是水
题。
Uva 10183:
Meaning:
给定两个整数,统计区间[a,b]内有多少个斐波那契数。
Thinking:
没有什么特殊的地方,由斐波拉切数的通项公式可估算出,在题目给出的数据范围内最多有60个左右的斐波拉切数,所有可直接处理出来,然后询问。
Code:
Uva 10213: Meaning: 椭圆上n个点,连成n*(n-1)/2条线段,最多可以把椭圆分成多少个部分。 Thinking: 公式:F(n) = C(n, 2) + C(n, 4) + 1 http://en.wikipedia.org/wiki/Dividing_a_circle_into_areas http://www.arbelos.co.uk/Papers/Chords-regions.pdf Code:
Uva 10198:
Meaning:
给出一个数n,问有多少个数的数字之和恰好为n。数字只包括1、2、3、4且1和4等价。
Thinking:
因为只有四个数字,所以用test[n]表示:有test[n]个数的数字之和恰好为n。
所以test[n - 1]表示:有test[n - 1]个数的数字之和恰好为n - 1;
test[n - 2]表示:有test[n - 2]个数的数字之和恰好为n - 2;
test[n - 3]表示:有test[n - 3]个数的数字之和恰好为n - 3;
test[n - 4]表示:有test[n - 4]个数的数字之和恰好为n - 4;
所以test[n] = test[n - 1] + test[n - 2] + test[n - 3] + test[n - 4];
又因为数字4和1等价,所以test[n - 1]等于test[n - 4];
所以test[n] = 2 * test[n - 1] + test[n - 2] + test[n - 3];
Code:
Uva 10157:
Meaning:
给出括号表达式的长度n,和深度d;求长度为n,深度为d的合法表达式的个数。长度和深度的定义题目均以给出。
Thinking:
由题意可知,当括号表达式的长度为奇数是,满足条件的表达式个数必定为0,同时若深度超过n个括号所能得到的合法表达式的最大深度时,结果也为0;所以,当长度为偶数时,用f[m][d]表示:括号对数为m,深度不超过d的
合法表达式的总数;假设E是一个深度为d,括号对数为m的合法表达式,则E的最左边的括号l,一定和某个右括号r匹配,把他们看做一组分界线,则这对括号将表达式分成两个部分,括号里面的和括号右边的。
E = ( X ) Y
假设左边部分由k对括号,则右边部分就有m - k -1对括号,表达式X的深度为d-1,表达式Y的最大深度为d,所以,括号对数为m,深度为d的合法表达式的总数为 f[m][d] - f[m][d - 1]。
而且f[m][d] = f[k][d - 1] * f[m - k - 1][d], 0 <= k <= m - 1
Code:
Uva 10247:
Meaning:
给出完全k叉树的深度d和分支因子k,统计有多少种方案给一棵完全k叉树中的每个节点标号。标号规则是:每个节点的标号小于它所有后代的标号。对于一颗n个节点的树,标号范围是(1, 2, 3, ···, n - 1, n)。
Thinking:
由题意知:根节点的标号一定是最小的,用f[i][j]表示:i叉树,j层时的方案数;用g[i][j]表示:i叉树,j层时的节点数;C(m, n)表示:m中取n的组合数。根节点用掉最小的一个标号后,还剩g[i][j] - 1 个标号,要将它分给i颗子树,每颗子树得到g[i][j - 1]个标号,且g[i][j] - 1 = i * g[i][j - 1]。
根据排列组合规律可以得出公式f[i][j] = f[i][j - 1] * C(k * g[i][j - 1], g[i][j - 1]),1 <= k <= i
Code:
Uva 10254:
Meaning:
四根柱子的汉诺塔问题,移动规则是标准汉诺塔的移动策略。给出盘子的数量N,求出最少的移动步数。
Thinking:
先把最小的圆盘移动到第四个柱子上,然后用标准汉诺塔的策略把剩下的N - 1个圆盘移动到目标柱子上,再把最小的圆盘移动到目标柱子上。然后打表会发现规律,相邻连个数的差是2的次幂,且2的m次幂将出现n + 1次。模拟增加的过程,预处理出所有数据。
Code:
Uva 10049:
Meaning:
自描述序列是唯一一个具有如下性质的不下降正整数子序列:对于任意正整数k,改序列恰好包含f(k)个k。对于给定的n,计算f(n)的值。
Thinking:
这个题最能体现出,组合数学题推公式类似于推状态转移方程的特点。这个序列的规律很明显,可以直接模拟f(n)增长的过程,但是1 <= n <= 2 000 000 000,所以本题的数据范围内不能直接模拟。但f(n)的增长缓慢,可以利用它的反函数g(f(n)) = n,但这个函数出现了一对多的情况,所以修改后g(f(n)) = max{n | f(n) = n},然后再求一次反函数f(n) = min{k | g(k) >= n},即可得到f(n)。由表可推出公式f(n) = max{f-1(n)} - max{f-1(n-1)},即f(n) = g(n) - g(n - 1);
Code:
Meaning:
按照题目给出的规则,从数轴上的x走到y,最少需要多少步。
Thinking:
算出前几个数后就能找到规律。
Code:
java写入文件:
学习用java处理大数很有必要,解组合数学的题就是一个找公式推规律的过程。推公式的过程
又类似于推导状态转移方程,再推出公式后,往往会发现题目的数据是超long long的,再用大数
方法来处理,代码很随意就能上200行,但是如果用java来写的话,推出公式,组合数学的题就是水
题。
Uva 10183:
Meaning:
给定两个整数,统计区间[a,b]内有多少个斐波那契数。
Thinking:
没有什么特殊的地方,由斐波拉切数的通项公式可估算出,在题目给出的数据范围内最多有60个左右的斐波拉切数,所有可直接处理出来,然后询问。
Code:
<pre name="code" class="java">import java.math.BigInteger;
import java.util.Scanner;
public class Main
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
BigInteger[] f = new BigInteger[600];
f[0] = new BigInteger("1");
f[1] = new BigInteger("2");
for(int i = 2; i < 600; i ++)
f[i] = f[i - 1].add(f[i - 2]);
for(;;)
{
BigInteger a, b;
int res = 0;
a = cin.nextBigInteger();
b = cin.nextBigInteger();
if(a.compareTo(BigInteger.ZERO) == 0 && b.compareTo(BigInteger.ZERO) == 0)
break;
for(int i = 0; i < 600; i ++)
if(f[i].compareTo(a) != -1 && f[i].compareTo(b) != 1)
res ++;
System.out.println(res);
}
}
}
Uva 10213: Meaning: 椭圆上n个点,连成n*(n-1)/2条线段,最多可以把椭圆分成多少个部分。 Thinking: 公式:F(n) = C(n, 2) + C(n, 4) + 1 http://en.wikipedia.org/wiki/Dividing_a_circle_into_areas http://www.arbelos.co.uk/Papers/Chords-regions.pdf Code:
import java.math.BigInteger;
import java.util.Scanner;
public class Main
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
int t;
t = cin.nextInt();
BigInteger a = new BigInteger("6");
BigInteger b = new BigInteger("18");
BigInteger c = new BigInteger("23");
BigInteger d = new BigInteger("24");
while(t-- > 0)
{
BigInteger N = cin.nextBigInteger();
BigInteger ans;
ans = (N.multiply(N).multiply(N).multiply(N).subtract(a.multiply(N.multiply(N).multiply(N))).add(c.multiply(N.multiply(N))).subtract(b.multiply(N)).add(d)).divide(d);
System.out.println(ans);
}
}
}
Uva 10198:
Meaning:
给出一个数n,问有多少个数的数字之和恰好为n。数字只包括1、2、3、4且1和4等价。
Thinking:
因为只有四个数字,所以用test[n]表示:有test[n]个数的数字之和恰好为n。
所以test[n - 1]表示:有test[n - 1]个数的数字之和恰好为n - 1;
test[n - 2]表示:有test[n - 2]个数的数字之和恰好为n - 2;
test[n - 3]表示:有test[n - 3]个数的数字之和恰好为n - 3;
test[n - 4]表示:有test[n - 4]个数的数字之和恰好为n - 4;
所以test[n] = test[n - 1] + test[n - 2] + test[n - 3] + test[n - 4];
又因为数字4和1等价,所以test[n - 1]等于test[n - 4];
所以test[n] = 2 * test[n - 1] + test[n - 2] + test[n - 3];
Code:
import java.math.BigInteger;
import java.util.Scanner;
public class Main004
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
BigInteger[] test = new BigInteger[1002];
test[1] = new BigInteger("2");
test[2] = new BigInteger("5");
test[3] = new BigInteger("13");
for (int i = 4; i <= 1000; i++)
{
test[i] = test[i - 1].add(test[i - 1]).add(test[i - 2]).add(test[i - 3]);
}
int n;
while (cin.hasNext())
{
n = cin.nextInt();
System.out.println(test[n]);
}
}
}
Uva 10157:
Meaning:
给出括号表达式的长度n,和深度d;求长度为n,深度为d的合法表达式的个数。长度和深度的定义题目均以给出。
Thinking:
由题意可知,当括号表达式的长度为奇数是,满足条件的表达式个数必定为0,同时若深度超过n个括号所能得到的合法表达式的最大深度时,结果也为0;所以,当长度为偶数时,用f[m][d]表示:括号对数为m,深度不超过d的
合法表达式的总数;假设E是一个深度为d,括号对数为m的合法表达式,则E的最左边的括号l,一定和某个右括号r匹配,把他们看做一组分界线,则这对括号将表达式分成两个部分,括号里面的和括号右边的。
E = ( X ) Y
假设左边部分由k对括号,则右边部分就有m - k -1对括号,表达式X的深度为d-1,表达式Y的最大深度为d,所以,括号对数为m,深度为d的合法表达式的总数为 f[m][d] - f[m][d - 1]。
而且f[m][d] = f[k][d - 1] * f[m - k - 1][d], 0 <= k <= m - 1
Code:
import java.math.BigInteger;
import java.util.Scanner;
public class Main
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
BigInteger[][] f = new BigInteger[160][160];
for (int i = 0; i <=150; i++)
{
f[0][i] = new BigInteger("1");
}
for (int i = 1; i <= 150; i++)
{
for (int j = 0; j <= 150; j++)
{
f[i][j] = new BigInteger("0");
}
}
for (int i = 1; i <= 150; i++)
{
for (int j = 1; j <= 150; j++)
{
for (int k =0; k < i; k++)
{
f[i][j] = f[i][j].add(f[k][j - 1].multiply(f[i - k -1][j]));
}
}
}
while (cin.hasNext())
{
int n = cin.nextInt(), d = cin.nextInt();
if (n % 2 == 1)
System.out.println("0");
else
{
n >>= 1;
System.out.println(f[n][d].add(f[n][d - 1].negate()));
}
}
}
}
Uva 10247:
Meaning:
给出完全k叉树的深度d和分支因子k,统计有多少种方案给一棵完全k叉树中的每个节点标号。标号规则是:每个节点的标号小于它所有后代的标号。对于一颗n个节点的树,标号范围是(1, 2, 3, ···, n - 1, n)。
Thinking:
由题意知:根节点的标号一定是最小的,用f[i][j]表示:i叉树,j层时的方案数;用g[i][j]表示:i叉树,j层时的节点数;C(m, n)表示:m中取n的组合数。根节点用掉最小的一个标号后,还剩g[i][j] - 1 个标号,要将它分给i颗子树,每颗子树得到g[i][j - 1]个标号,且g[i][j] - 1 = i * g[i][j - 1]。
根据排列组合规律可以得出公式f[i][j] = f[i][j - 1] * C(k * g[i][j - 1], g[i][j - 1]),1 <= k <= i
Code:
import java.math.BigInteger;
import java.util.Scanner;
public class Main
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
BigInteger[][] f = new BigInteger[30][30];
int[][] g = new int[30][30];
for(int i = 1; i <= 21; i++)
{
f[i][0] = new BigInteger("1");
g[i][0] = 1;
for(int j = 1; i * j <= 21; j++)
{
f[i][j] = new BigInteger("1");
g[i][j] = i * g[i][j - 1];
g[i][j]++;
for(int k = i; k >= 1; k--)
{
f[i][j] = f[i][j].multiply(f[i][j - 1].multiply(C(k * g[i][j - 1], g[i][j - 1])));
}
}
}
while(cin.hasNext())
{
int k = cin.nextInt();
int d = cin.nextInt();
System.out.println(f[k][d]);
}
}
public static BigInteger C(int m, int n)
{
if(m - n < n)
n = m - n;
BigInteger res = new BigInteger("1");
for(int i = 1; i <= n; i++)
res = res.multiply(BigInteger.valueOf(m - i + 1)).divide(BigInteger.valueOf(i));
return res;
}
}
Uva 10254:
Meaning:
四根柱子的汉诺塔问题,移动规则是标准汉诺塔的移动策略。给出盘子的数量N,求出最少的移动步数。
Thinking:
先把最小的圆盘移动到第四个柱子上,然后用标准汉诺塔的策略把剩下的N - 1个圆盘移动到目标柱子上,再把最小的圆盘移动到目标柱子上。然后打表会发现规律,相邻连个数的差是2的次幂,且2的m次幂将出现n + 1次。模拟增加的过程,预处理出所有数据。
Code:
import java.math.BigInteger;
import java.util.Scanner;
public class Main
{
public static void main(String[] args)
{
Scanner cin = new Scanner(System.in);
BigInteger[] ans = new BigInteger[10009];
BigInteger d = new BigInteger("1");
ans[0] = new BigInteger("0");
int n, k = 0, ci = 1;
for (int i = 1; i <= 10000; i++)
{
ans[i] = ans[i - 1].add(d);
k++;
if (k == ci)
{
k = 0;
ci++;
d = d.multiply(BigInteger.valueOf(2));
}
}
while (cin.hasNext())
{
n = cin.nextInt();
System.out.println(ans[n]);
}
}
}
Uva 10049:
Meaning:
自描述序列是唯一一个具有如下性质的不下降正整数子序列:对于任意正整数k,改序列恰好包含f(k)个k。对于给定的n,计算f(n)的值。
Thinking:
这个题最能体现出,组合数学题推公式类似于推状态转移方程的特点。这个序列的规律很明显,可以直接模拟f(n)增长的过程,但是1 <= n <= 2 000 000 000,所以本题的数据范围内不能直接模拟。但f(n)的增长缓慢,可以利用它的反函数g(f(n)) = n,但这个函数出现了一对多的情况,所以修改后g(f(n)) = max{n | f(n) = n},然后再求一次反函数f(n) = min{k | g(k) >= n},即可得到f(n)。由表可推出公式f(n) = max{f-1(n)} - max{f-1(n-1)},即f(n) = g(n) - g(n - 1);
Code:
#include <stdio.h>
#include <algorithm>
using namespace std;
const int M = 700000;
long long g[M];
int main()
{
int n, i;
for (g[1] =1, g[2] = i = 3; i < M; i++)
{
g[i] = g[i - 1] + (lower_bound(g + 1, g + i, i) - g);
}
while (scanf("%d", &n), n)
{
printf("%d\n", lower_bound(g + 1, g + M, n) - g);
}
return 0;
}
Meaning:
按照题目给出的规则,从数轴上的x走到y,最少需要多少步。
Thinking:
算出前几个数后就能找到规律。
Code:
#include <stdio.h>
#include <math.h>
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n, m;
scanf("%d%d", &n, &m);
int dis = m - n;
if (!dis)
{
printf("0\n");
continue;
}
int s = (int)sqrt((double)dis);
if (s * s == dis)
s = 2 * s - 1;
else if (s * (s + 1) < dis)
s = 2 *s + 1;
else
s *= 2;
printf("%d\n", s);
}
return 0;
}
java写入文件:
try
{
File test = new File("test.txt");
PrintStream out = new PrintStream(new FileOutputStream(test));
System.setOut(out);
}
catch(Exception e){}