排列组合
public class BaseDemo {
/** * 在 XxY 的方格中,以左上角格子为起点,右下角格子为终点, * 每次只能向下走或者向右走,请问一共有多少种不同的走法 * 给定两个正整数 int x,int y,请返回走法数目。保证 x+y 小于等于 12。 */
public int countWays(int x, int y) {
//总的步数
int sum = x + y - 2;
//向下的步数,这里也可以是 right = x-1
int down = y - 1;
return fac(sum) / (fac(down) * fac(sum - down));
}
// 阶乘函数
private int fac(int num) {
int sum = 1;
for (int i = 1; i <= num; i++) {
sum *= i;
}
return sum;
}
/** * n 个人站队,他们的编号依次从 1 到 n, * 要求编号为 a 的人必须在编号为 b 的人的左边,但不要求一定相邻, * 请问共有多少种排法?第二问如果要求 a 必须在 b 的左边,并且一定要相邻,请问一共有多少种排法? * 给定人数 n 及两个人的编号 a 和 b,请返回一个两个元素的数组,其中两个元素依次为两个问题的答案。 * 保证人数小于等于 10。 */
public int[] getWays(int n, int a, int b) {
// 非左即右,答案为所有组合结果除以二
// 两人绑定为一个人,答案为少了一个人的所有组合结果
return new int[]{fac(n) / 2, fac(n - 1)};
}
/** * A (A 也是他的编号) 是一个孤傲的人,在一个 n 个人 (其中编号依次为 1 到 n) 的队列中, * 他于其中的标号为 b 和标号 c 的人都有矛盾,所以他不会和他们站在相邻的位置。 * 现在问你满足 A 的要求的对列有多少种? * 给定人数 n 和三个人的标号 A,b 和 c,请返回所求答案,保证人数小于等于 11 且大于等于 3。 */
public int getWays(int n, int A, int b, int c) {
// 所有组合
int sum = fac(n);
// 与 c 一起的所有组合(左边 + 右边)
int ac = fac(n - 1) * 2;
// 与 b 一起的所有组合(左边 + 右边)
int ab = fac(n - 1) * 2;
// ac 与 ab 的重复(abc 视作一个人)
int overlap = fac(n - 2) * 2;
return sum - ac - ab + overlap;
}
/** * n 颗相同的糖果,分给 m 个人,每人至少一颗,问有多少种分法。 * 给定 n 和 m,请返回方案数,保证 n 小于等于 12,且 m 小于等于 n。 */
public int getWays(int n, int m) {
// n 个糖,n-1 个空隙,m 个人, m-1 个隔板
// C^{m-1}_{n-1}
return fac(n - 1) / (fac(m - 1) * fac(n - m));
}
/** * 1. 假设有 n 对左右括号,请求出合法的排列有多少个?合法是指每一个括号都可以找到与之配对的括号, * 比如 n=1 时,() 是合法的,但是)(为不合法。 * 给定一个整数 n,请返回所求的合法排列数。保证结果在 int 范围内。 * 2. n 个数进出栈的顺序有多少种?假设栈的容量无限大。 * 思想:卡特朗数,把进栈记为 ↓,出栈记为↑, 要出栈必须有相应的进栈操作,跟括号那道题就一样了,下同 * 3. 2n 个人排队买票,n 个人拿 5 块钱,n 个人拿 10 块钱,票价是 5 块钱 1 张, * 每个人买一张票,售票员手里没有零钱,问有多少种排队方法让售票员可以顺利卖票。 * 4. 求 n 个无差别的节点构成的二叉树有多少种不同的结构? */
public int countLegalWays(int n) {
int A = 1;
for (int i = n + 1; i <= n + n; ++i)
A *= i;
for (int i = 2; i <= n + 1; ++i)
A /= i;
return A;
}
// 12 个高矮不同的人,排成两排,每排必须是从矮到高排列,
// 而且第二排比对应的第一排的人高,问排列方式有多少种?
public int countWays(int n) {
int A = 1;
for (int i = n / 2 + 1; i <= n; ++i)
A *= i;
for (int i = 2; i <= n / 2 + 1; ++i)
A /= i;
return A;
}
// 有 n 个信封,包含 n 封信,现在把信拿出来,再装回去,
// 要求每封信不能装回它原来的信封,问有多少种装法?
public int countWays2(int n) {
if (n == 1) {
return 0;
}
long[] res = new long[n + 1];
res[2] = 1;
for (int i = 3; i <= n; i++) {
res[i] = (i - 1) * (res[i - 1] + res[i - 2]) % 1000000007;
}
return (int) res[n];
}
}
概率
import java.util.Random;
public class ProbabilityDemo {
/** * 有 2k 只球队,有 k-1 个强队,其余都是弱队,随机把它们分成 k 组比赛, * 每组两个队,问两强相遇的概率是多大? * 给定一个数 k,请返回一个数组,其中有两个元素,分别为最终结果的分子和分母,请化成最简分数 */
public int[] calc(int k) {
// 计算总组合数
int sum = 1;
for (int i = 2 * k - 1; i > 1; i -= 2) {
sum *= i;
}
// 计算没有相遇数
int notMeet = 1;
int num = k + 1;
for (int j = 1; j <= k - 1; j++) {
notMeet *= (num--);
}
int gcd = gcd(sum - notMeet, sum);
return new int[]{(sum - notMeet) / gcd, sum / gcd};
}
/** * n 只蚂蚁从正 n 边形的 n 个定点沿着边移动,速度是相同的,问它们碰头的概率是多少? */
public int[] collision(int n) {
// 每个蚂蚁两个方向, n 个蚂蚁 2^n 个方向,同时顺时针或同时逆时针不相遇
int up = (int) Math.pow(2, n) - 2;
int down = (int) Math.pow(2, n);
int gcd = gcd(up, down);
return new int[]{up / gcd, down / gcd};
}
// 辗转相除法求最大公约数
public int gcd(int meet, int total) {
if (meet % total == 0) {
return total;
}
return gcd(total, meet % total);
}
/** * 给定一个等概率随机产生 1~5 的随机函数, * 除此之外,不能使用任何额外的随机机制,请实现等概率随机产生 1~7 的随机函数。 */
private static Random rand = new Random(123456);
// 随机产生[1,5]
private int rand5() {
return 1 + rand.nextInt(5);
}
// 通过rand5实现rand7
public int randomNumber() {
// rand5() - 1 随机生成 0、1、2、3、4
// (rand5() - 1) * 5 随机生成 0、5、10、15、20
// (rand5() - 1) * 5 + rand5() 随机生成 0、1、2、3、4、……、24
int res = (rand5() - 1) * 5 + rand5();
// 保证 7 的倍数
while (res > 21) {
res = (rand5() - 1) * 5 + rand5();
}
return res % 7 + 1;
}
/** * 给定一个以 p 概率产生 0,以 1-p 概率产生 1 的随机函数 RandomP::f (),p 是固定的值, * 但你并不知道是多少。除此之外也不能使用任何额外的随机机制, * 请用 RandomP::f () 实现等概率随机产生 0 和 1 的随机函数。 */
private static double p = new Random().nextFloat();
// 随机概率p
public static int f() {
return new Random().nextFloat() < p ? 0 : 1;
}
public int random01() {
while (true) {
// 虽然概率 p 不知道
// 但是 01 或 10 的概率 p(1-p)是一样的
int a = f();
int b = f();
if (a != b) {
return a > b ? 1 : 0;
}
}
}
/** * 假设函数 f () 等概率随机返回一个在 [0,1) 范围上的浮点数, * 那么我们知道,在 [0,x) 区间上的数出现的概率为 x (0<x≤1)。 * 给定一个大于 0 的整数 k,并且可以使用 f () 函数, * 请实现一个函数依然返回在 [0,1) 范围上的数, * 但是在 [0,x) 区间上的数出现的概率为 x 的 k 次方。 */
private Random rand1 = new Random(12345);
public double f1() {
return rand1.nextFloat();
}
// 请调用f()函数实现
public double random(int k, double x) {
double max = -1;
// 调用 k 次取较大的值
// 一次实验中,假如产生一个数 X0 ,那么有 P (X0<x)=x
// 两次实验中,假如产生了两个数 X1,X2, 那么有 P(X1<x)=x,P(X2<x)=x
// 而 P ((X1<x) && (X2<x)) = x^2, 等价于 X1,X2 中的最大数 < x, 此时的概率是 x^2
// 因此要返回最大的数
while (k-- > 0) {
max = Math.max(f1(), max);
}
return max;
}
/** * 给定一个长度为 N 且没有重复元素的数组 arr 和一个整数 M, * 实现函数等概率随机打印 arr 中的 M 个数。 */
public int[] print(int[] arr, int N, int M) {
int[] res = new int[M];
int index = 0;
while (index < M) {
// 生成随机数
int rand = new Random().nextInt(N - index);
res[index] = arr[rand];
// 原数组已复制的数与最后交换(?不明白为什么直接覆盖不交换不能通过
swap(arr, rand, N - index - 1);
index ++;
}
return res;
}
private void swap(int[] arr, int pos, int last) {
int tmp;
tmp = arr[pos];
arr[pos] = arr[last];
arr[last] = tmp;
}
/** * 有一个机器按自然数序列的方式吐出球,1 号球,2 号球,3 号球等等。 * 你有一个袋子,袋子里最多只能装下 K 个球,并且除袋子以外,你没有更多的空间, * 一个球一旦扔掉,就再也不可拿回。设计一种选择方式,使得当机器吐出第 N 号球的时候, * 你袋子中的球数是 K 个,同时可以保证从 1 号球到 N 号球中的每一个, * 被选进袋子的概率都是 K/N。举一个更具体的例子,有一个只能装下 10 个球的袋子, * 当吐出 100 个球时,袋子里有 10 球,并且 1~100 号中的每一个球被选中的概率都是 10/100。 * 然后继续吐球,当吐出 1000 个球时,袋子里有 10 个球, * 并且 1~1000 号中的每一个球被选中的概率都是 10/1000。继续吐球, * 当吐出 i 个球时,袋子里有 10 个球,并且 1~i 号中的每一个球被选中的概率都是 10/i。 * 也就是随着 N 的变化,1~N 号球被选中的概率动态变化成 k/N。 * N 个球时袋子中的球的编号返回。 */
private int [] selected = null;
// 每次拿一个球都会调用这个函数,N表示第N次调用
public int[] carryBalls(int N, int k) {
if (selected == null) selected = new int[k];
// 1. 处理 1~k 号球时直接放入袋子
if (N <= k) {
selected[N - 1] = N;
return selected;
}
// 2. 处理第 N 号球时,以 k/N 的概率决定是否将第 N 号球放入袋子
// 若决定不放则直接丢弃第 N 号球
// 若决定放入则从袋子里的 k 个球中随机丢弃一个,然后把第 N 号球放入袋子
if (k > rand.nextInt(N)) {
selected[rand.nextInt(k)] = N;
}
return selected;
}
}