【算法&数据结构体系篇class39】卡特兰数

一、卡特兰数

卡特兰数又称卡塔兰数,英文名Catalan number是组合数学中一个常出现在各种计数问题中出现的数列。其前几项为:

1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190, 6564120420, 24466267020, 91482563640, 343059613650, 1289904147324, 4861946401452, ...

 

k(0) = 1, k(1) = 1时,如果接下来的项满足:

k(n) = k(0) * k(n - 1) + k(1) * k(n - 2) + ... + k(n - 2) * k(1) + k(n - 1) * k(0)

或者

k(n) = c(2n, n) - c(2n, n-1)

或者

k(n) = c(2n, n) / (n + 1)

说这个表达式,满足卡特兰数,常用的是范式123几乎不会使用到

二、题目一

假设给你N0,和N1,你必须用全部数字拼序列

返回有多少个序列满足:任何前缀串,1的数量都不少于0的数量

 

package class39;

import java.util.LinkedList;

/**
 * 假设给你N个0,和N个1,你必须用全部数字拼序列
 *
 * 返回有多少个序列满足:任何前缀串,1的数量都不少于0的数量
 *
 * 题型与成对括号是一样的。 有n个左括号,n个右括号 拼接全部字符()排序列 返回多少个满足的序列
 * 潜台词有效序列就是要 任何前缀串 左括号数量要不少于右括号的数量 比如 ())... 这里来到第三个字符 九出现右括号2个>左括号 就是无效的
 * 那么有效的排列是多少 根据排列组合  C(2N,N)  就是全部组合数 总共2n个字符,左括号n个 要在2n个的位置找n个 所以就是C 2N ,N
 * 那么无效的怎么算: 这里我们有个结论 就是 两个集合的数 分别都各自有一一对应的数 那么两集合就是相等数的
 * 我们将一个 无效的排序 在出现前缀串无效的后一个开始 后面的括号都取相反, 比如())(() 一开始是3个左右括号
 * 来到第三个 出现无效情况 ()),此时前缀肯you定是右括号个数=左括号个数+1
 * 接着后面的括号 (() 都取反 变成 ))( 这里也变成 右括号=左括号+1
 * 最终变成 右括号+1 = 左括号-1 大2个   而这种集合 就是一一对应无效排序的 所以算出 2n个中选N+1位置排列组合
 * 就是C(2N,N-1) 等价于无效排序组合是这么多
 * 那么有效的就是 C(2N,N) - C(2N,N+1)   来自公式一演算
 *
 *
 *
 * 这个题型是运用 卡特兰数的公式求解
 * k(0) = 1, k(1) = 1时,如果接下来的项满足:
 * k(n) = k(0) * k(n - 1) + k(1) * k(n - 2) + ... + k(n - 2) * k(1) + k(n - 1) * k(0)
 * 或者
 * k(n) = c(2n, n) - c(2n, n-1)
 * 或者
 * k(n) = c(2n, n) / (n + 1)
 * 就说这个表达式,满足卡特兰数,常用的是范式1和2,3几乎不会使用到
 */
public class Ways10 {

    //根据公式三直接算出结果
    public static long ways2(int n){
        //base case  0个 1个的时候 都能满足 有1种
        if(n < 0) return 0;
        if(n < 2)  return 1;

        //定义两个辅助变量
        long a = 1;
        long b = 1;
        long n2 = n << 1;
        //C(2N,N) 就是  2n! / (2n - n)! * n! = 2n! / n ! * n !
        //这里2n!可以约掉分母一个 n! 然后就剩下
        // (n+1)*(n+2)*(n+3)..*(2n) / n !

        //所以根据这个约分后的结果 进行遍历
        for(int i = 1; i <= n2; i++){
            if(i <= n){
                //a就是 n! 累乘1...n  作为分母
                a *= i;
            } else {
                //b是到了 n+1开始累乘到2n 就是约分后的分子
                b *= i;
            }
        }
        return (b / a) / (n + 1);
    }

    public static long ways1(int N) {
        int zero = N;
        int one = N;
        LinkedList<Integer> path = new LinkedList<>();
        LinkedList<LinkedList<Integer>> ans = new LinkedList<>();
        process(zero, one, path, ans);
        long count = 0;
        for (LinkedList<Integer> cur : ans) {
            int status = 0;
            for (Integer num : cur) {
                if (num == 0) {
                    status++;
                } else {
                    status--;
                }
                if (status < 0) {
                    break;
                }
            }
            if (status == 0) {
                count++;
            }
        }
        return count;
    }

    public static void process(int zero, int one, LinkedList<Integer> path, LinkedList<LinkedList<Integer>> ans) {
        if (zero == 0 && one == 0) {
            LinkedList<Integer> cur = new LinkedList<>();
            for (Integer num : path) {
                cur.add(num);
            }
            ans.add(cur);
        } else {
            if (zero == 0) {
                path.addLast(1);
                process(zero, one - 1, path, ans);
                path.removeLast();
            } else if (one == 0) {
                path.addLast(0);
                process(zero - 1, one, path, ans);
                path.removeLast();
            } else {
                path.addLast(1);
                process(zero, one - 1, path, ans);
                path.removeLast();
                path.addLast(0);
                process(zero - 1, one, path, ans);
                path.removeLast();
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("test begin");
        for (int i = 0; i < 10; i++) {
            long ans1 = ways1(i);
            long ans2 = ways2(i);
            if (ans1 != ans2) {
                System.out.println("Oops!");
            }
        }
        System.out.println("test finish");
    }
}

 

三、题目二

N个二叉树节点,每个节点彼此之间无任何差别

返回由N个二叉树节点,组成的不同结构数量是多少?

 

package class39;

/**
 * 有N个二叉树节点,每个节点彼此之间无任何差别
 *
 * 返回由N个二叉树节点,组成的不同结构数量是多少?
 *
 * 这道题也是 卡特兰数的变种,由公式一推算出来
 * 1. 左子树为0 右子树就为n-1  根节点占了1个节点 能匹配出 k(0) * k(n - 1) 种树型
 * 2. 左子树为1 右子树就为n-2   匹配出 k(1) * k(n - 2) 种...
 * 以此类推 将这么些类型累加  k(n) = k(0) * k(n - 1) + k(1) * k(n - 2) + ... + k(n - 2) * k(1) + k(n - 1) * k(0)
 * 空树 k(0) , 只有根节点树k(1) 值都为1 1种   符合公式一
 *
 * 实际上我们用三个公式任意一个求解都可
 *
 */
public class DifferentBTNum {

    //公式一 得出多少种树
    public static long num1(int n){
        if(n < 0) return 0;
        if(n < 2) return 1;

        //定义一个数组 包含0..n个数  索引  那么长度就是n+1
        long[] dp = new long[n+1];
        //初始化 0 和 1个数的时候 只有一种树结构
        dp[0] = 1;
        dp[1] = 1;
        //开始遍历填充 i=2,3,4....
        for(int i =2; i <= n; i++){
            for(int leftSize = 0; leftSize < i; leftSize++){
                //内层循环就是指 左边树的结果  乘以 总数n - 左边的再-1就是右树的结果 总共的n-1个节点  因为根节点要保留一个 这个公式就是这么算的
                dp[i] += dp[leftSize] * dp[i-1-leftSize];
            }
        }

        //最后返回下标N位置 表示n个节点的树的种树
        return dp[n];
    }

    //公式三 得出多少种树
    public static long num2(int n){
        if(n < 0) return 0;
        if(n < 2) return 1;

        //定义辅助变量
        long a = 1;
        long b = 1;

        //C(2N,N) 就是  2n! / (2n - n)! * n! = 2n! / n ! * n !
        //这里2n!可以约掉分母一个 n! 然后就剩下
        // (n+1)*(n+2)*(n+3)..*(2n) / n !
        // 这里我们加入一个方法目的就是防止数字过大溢出 所以边进行除以两数的最大公约数
        // 就是将每一个数 进行求最大公约数 每次都除以这个最大公约数
        for(int i = 1 , j = n+1; i <= n;i++, j++){
            //i 从1 累乘到n  赋值给a 作为分母
            a *= i;
            //j  从n+1 累乘到 2n  赋值给b 作为 分子
            b *= j;
            long gcd = gcd(a,b); //求每次的最大公约数
            a /= gcd;
            b /= gcd;
        }
        //最后按公式返回结果
        return (b / a) / (n+1);
    }

    //求两数的最大公约数
    public static long gcd(long a, long b){
        return b == 0 ? a : gcd(b, a %b);
    }

    public static void main(String[] args) {
        System.out.println("test begin");
        for (int i = 0; i < 15; i++) {
            long ans1 = num1(i);
            long ans2 = num2(i);
            if (ans1 != ans2) {
                System.out.println("Oops!");
            }
        }
        System.out.println("test finish");
    }
}

四、题目三

// arr中的值可能为正,可能为负,可能为0
// 自由选择arr中的数字,返回能不能累加得到sum

 

package class39;

import class33.Hash;

import java.util.HashMap;
import java.util.HashSet;

/**
 *
 // arr中的值可能为正,可能为负,可能为0
 // 自由选择arr中的数字,返回能不能累加得到sum
 */
public class IsSumTest {
    /**
     * 暴力递归: arr[0..index] 自由选择是否可以累加到sum
     * @return
     */
    public static boolean process1(int[] arr, int index, int sum){
        //base case: 累加减到0时, 自由选择 可以不选 得到0  所以返回true
        if(sum == 0) return true;

        //base case: 如果0..index 越界了 也就是没有数的时候 sum也不为0的情况下 是不能累加到Sum 返回false
        if(index == -1) return false;

        //分析常规情况 当前index 有数 并且sum还不为0
        //当前Index数 不选  那么就等价于 0..index-1的是否能累加到sum
        boolean p1 = process1(arr,index-1,sum);
        //选 那么就等价于 0...index-1的数 是否能累加到sum - arr[index]
        boolean p2 = process1(arr,index-1, sum - arr[index]);

        return p1 || p2;
    }

    //方法一: 递归
    public static boolean isSum1(int[] arr, int sum){
        //base case: 不选数字 满足累加到0 返回true
        if(sum == 0) return true;
        if(arr == null || arr.length == 0) return false;  //数组没有数 返回false

        //调用递归函数 整个数组 0...n-1开始递归判断是否存在累加和为sum
        return process1(arr, arr.length - 1, sum);
    }


    /**
     * 递归+记忆化搜索 增加一个缓存机制  HashMap 存储每个 0..index 自由选择 是否可以累加到sum
     * @param arr
     * @param index
     * @param sum
     * @param map
     * @return
     */
    public static boolean process2(int[] arr, int index, int sum, HashMap<Integer,HashMap<Integer,Boolean>> map){
        if(map.containsKey(index) && map.get(index).containsKey(sum)){
            //利用记忆化搜索哈希表 先在缓存表找 index 累加到sum的情况  如果存在记录 直接就返回 0...index 是否能累加到sum的结果 加快速度
            return map.get(index).get(sum);
        }

        //base case: 累加减到0时, 自由选择 可以不选 得到0  所以返回true
        if(sum == 0) return true;

        //base case: 如果0..index 越界了 也就是没有数的时候 sum也不为0的情况下 是不能累加到Sum 返回false
        if(index == -1) return false;

        //分析常规情况 当前index 有数 并且sum还不为0  不选 与 选  两种情况 其一有累加和符合即可
        //当前Index数 不选  那么就等价于 0..index-1的是否能累加到sum
        //选 那么就等价于 0...index-1的数 是否能累加到sum - arr[index]
        boolean ans = process2(arr,index-1,sum,map) || process2(arr, index-1, sum-arr[index], map);

        //刷新当前Index 哈希表 如果不存在index的记录 那么就先增加这个记录
        if(!map.containsKey(index)){
            map.put(index,new HashMap<>());
        }
        //最后再更新index 累加到sum 是否存在的记录
        map.get(index).put(sum,ans);
        return ans;
    }

    //方法二: 递归+记忆化搜索 其实就是动态规划了
    public static boolean isSum2(int[] arr, int sum){
        //base case: 不选数字 满足累加到0 返回true
        if(sum == 0) return true;
        if(arr == null || arr.length == 0) return false;  //数组没有数 返回false

        //调用递归函数 整个数组 0...n-1开始递归判断是否存在累加和为sum
        return process2(arr, arr.length - 1, sum, new HashMap<>());
    }

    //方法三: 经典动态规划
    public static boolean isSum3(int[] arr, int sum){
        //base case: 不选数字 满足累加到0 返回true
        if(sum == 0) return true;
        if(arr == null || arr.length == 0) return false;  //数组没有数 返回false

        //递归的两个变量参数 index sum  范围 index 就是0..arr.length-1
        //sum 这里注意了 因为题目存在负数 所以 sum的负数需要考虑进来 形成长度是 最小是最小负数和 最大是最大正数和的长度
        int min =0;   //定义两个数 分别表示 数组中 累加和最小和最大
        int max =0;
        for(int num : arr){
            if(num < 0){
                min += num;
            }else {  //这里num=0 加在min max都无所谓 值不影响
                max += num;
            }
        }
        //定义dp数组 行表示 数字  列表示sum和的范围  前面求出最小和最大 那么区间都多少个就是 大-小+1个
        //表示 dp[i][j]  0..i 数字自由选择 凑到和为j的是否存在 存在为true
        int n = arr.length;
        boolean[][] dp = new boolean[n][max - min + 1];

        //这里可以得到sum的越界判断 sum 最小不能小于min  最大不能大于max 否则就是无效的
        if(sum < min || sum > max) return false;

        //base case: 在0..index 中 不选数字 就都可以凑到 0数 值为true
        //也就是每一行的 -min列 就是true  因为前面存在负数 我们的数组的列 长度是包括了负数的 相当于是 min...0...max 这样的一个sum值
        //那么sum=0的时候 就是列在 dp[i][-min]了 因为min是负数 所以是- 才能来到 0的位置
        dp[0][-min] = true;
//        for(int i = 0; i < n; i++){
//            dp[i][-min] = true;
//        }
        //base case: 在0..0 中 也就是首个位置数字 刚好凑齐的sum 就是arr[i]这个值 由于前面负数的原因 列要来到这个值的时候 要-min 才能横移到对应位置
        dp[0][arr[0] - min] = true;

        //分析情况 递归的依赖情况: index 行 依赖于 index-1行  也就是第一行已经完善好 前面的base case 接着往下填充
        //从第二行 第一列开始
        for(int i = 1; i < n; i++){
            for(int j = min; j <= max; j++){
                //情况1 当前数字不取 那么依赖就是下个位置0...i-1数字
                dp[i][j - min] = dp[i-1][j -min];

                //情况2 当前数字取 取的话 注意这个边界溢出要做判断
                //一开始是和 j-min  取了数 就要减去数arr[i]  减完之后 要在dp数量列范围内 索引范围: 0,max-min
                int rest = j-min - arr[i];

                dp[i][j-min] |=(rest >=0 && rest <= max - min && dp[i-1][rest]) ;
            }
        }
        //调用就是 0...n-1位置 和得到和sum的结果 行就是最后一行n-1  列由于存在负数 我们需要把负数占的列不上 -min min是负数所以是-才能横移到对应sum的位置
        return dp[n-1][sum-min];
    }

    // arr中的值可能为正,可能为负,可能为0
    // 自由选择arr中的数字,能不能累加得到sum
    // 分治的方法
    // 如果arr中的数值特别大,动态规划方法依然会很慢
    // 此时如果arr的数字个数不算多(40以内),哪怕其中的数值很大,分治的方法也将是最优解
    public static boolean isSum4(int[] arr, int sum) {
        //base case: 不选数字 满足累加到0 返回true
        if(sum == 0) return true;
        if(arr == null || arr.length == 0) return false;  //数组没有数 返回false
        if (arr.length == 1) {                 //数组长度1 只有一个数 这个数就等于sum和就表示符合存在
            return arr[0] == sum;
        }

        //进行二分思想 分治
        int N = arr.length;
        int mid = N >> 1;
        //定义左右两边收集到的sum值 调用递归左边0...mid 右边mid+1...n-1  递归完成后就填充好两边能累加的和 保存在左右集合中
        HashSet<Integer> leftSum = new HashSet<>();
        HashSet<Integer> rightSum = new HashSet<>();
        process4(arr, 0, mid, 0, leftSum);
        process4(arr, mid+1, N-1, 0, rightSum);
        //接着判断是否存在 就是 左边集合 存在sum   右边集合存在sum  或者 左边加右边的和是sum 都是表示存在的 就返回true
        for (int l : leftSum) {
            //这里有个小细节 就是 左边和右边单独的判断 实际也是包含得了 虽然这里是遍历左集合 取右集合中存在sum-l
            //但是我们入参的时候 参数pre求和是从0开始的 所以,左右集合 都会存在一个0的数
            //假设左 右 各自存在 有sum 的数  那么 sum-l 必然也是能取到
            //比如 l = sum   sum-l=0 右集合存在0这个值
            //如果是 右集合存在sum  那么就是 sum-l=sum  l=0 左集合存在0这个数
            if (rightSum.contains(sum - l)) {
                return true;
            }
        }
        //否则没有满足的就返回false
        return false;
    }

    /**
     * 二分思想 递归  从i..end 自由选择 能累加得到pre 的数加到有序表记录
     * @param arr
     * @param i
     * @param end
     * @param pre
     * @param ans
     */
    public static void process4(int[] arr, int i, int end, int pre, HashSet<Integer> ans) {
        if (i > end) {
            //当前遍历的区间越界没有数了 就直接把能累加的和pre加入集合
            ans.add(pre);
        } else {
            //如果区间大于1个数 那么分析 不取    取 两种情况
            process4(arr, i + 1, end, pre, ans);
            process4(arr, i + 1, end, pre + arr[i], ans);
        }
    }

    // 为了测试
    // 生成长度为len的随机数组
    // 值在[-max, max]上随机
    public static int[] randomArray(int len, int max) {
        int[] arr = new int[len];
        for (int i = 0; i < len; i++) {
            arr[i] = (int) (Math.random() * ((max << 1) + 1)) - max;
        }
        return arr;
    }

    // 对数器验证所有方法
    public static void main(String[] args) {
        int N = 20;
        int M = 100;
        int testTime = 100000;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int size = (int) (Math.random() * (N + 1));
            int[] arr = randomArray(size, M);
            int sum = (int) (Math.random() * ((M << 1) + 1)) - M;
            boolean ans1 = isSum1(arr, sum);
            boolean ans2 = isSum2(arr, sum);
            boolean ans3 = isSum3(arr, sum);
            boolean ans4 = isSum4(arr, sum);
            if (ans1 ^ ans2 || ans3 ^ ans4 || ans1 ^ ans3) {
                System.out.println("出错了!");
                System.out.print("arr : ");
                for (int num : arr) {
                    System.out.print(num + " ");
                }
                System.out.println();
                System.out.println("sum : " + sum);
                System.out.println("方法一答案 : " + ans1);
                System.out.println("方法二答案 : " + ans2);
                System.out.println("方法三答案 : " + ans3);
                System.out.println("方法四答案 : " + ans4);
                break;
            }
        }
        System.out.println("测试结束");
    }

}

五、总结

1)如果像题目一一样,这个太明显了,你一定能发现

成对括号问题

2)看看题目是不是类题目二问题,比如:

人员站队问题

出栈入栈问题

需要敏感度!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值