蓝桥杯算法训练 数字游戏 组合数和暴力两种解法

试题 算法训练 数字游戏

资源限制

时间限制:1.0s 内存限制:256.0MB

问题描述

给定一个1~N的排列a[i],每次将相邻两个数相加,得到新序列,再对新序列重复这样的操作,显然每次得到的序列都比上一次的序列长度少1,最终只剩一个数字。
  例如:
  3 1 2 4
  4 3 6
  7 9
  16
  现在如果知道N和最后得到的数字sum,请求出最初序列a[i],为1~N的一个排列。若有多种答案,则输出字典序最小的那一个。数据保证有解。

输入格式

第1行为两个正整数n,sum

输出格式

一个1~N的一个排列

样例输入

4 16

样例输出

3 1 2 4

数据规模和约定

0 < n < = 10 00<n<=10

题解

首先我们大致浏览了一下题目,确定这个肯定是一个dfs的题目,而且是个很经典的全排列问题,唯一的区别就是我们排列以后要判断按照题目要求的sum

这边要记住:在进行加法使用的那个数组要拷贝一个备份的,否则改变了原数组,答案就错了

package com.lanqiao;

import java.util.Arrays;
import java.util.Scanner;

/**
 * @author 王宇哲
 */
public class 算法训练_数字游戏 {
    static int n;
    static int sum;

    /**
     * 标记是否已经找到满足条件的排列方式
     */
    static boolean hasFounded = false;

    /**
     * 标记该点是否访问过
     */
    static boolean[] book;

    /**
     * 存储排列数的容器
     */
    static int[] nums;

    /**
     * 深搜模板
     * @param depth 搜索的深度,初始为1,0 深度留空,为了给求和留出位置
     */
    static void dfs(int depth){

        //如果已经找到了排列,就不运行了
        if (hasFounded){
            return;
        }

        //如果已经枚举到了最后一位,就开始判断排列是否满足条件
        if(depth == n + 1){

            //拷贝数组
            int[] num2 = Arrays.copyOf(nums,n+2);

            //逐层运算
            for(int i=1;i<n;i++){
                for(int j=1;j<=n-i+1;j++){
                    num2[j] = num2[j]+num2[j+1];
                }
            }
            
            //如果满足条件直接输出
            if(num2[1] == sum){
                for(int i=1;i<=n;i++){
                    System.out.print(nums[i]+" ");
                }
                
                //标记找到
                hasFounded = true;
            }
            return;
        }

        for(int i = 1;i<=n;i++){
            //该点未被访问而且排列尚未找到,就继续运行
            if(!book[i] && !hasFounded){
                
                //深搜模板
                book[i] = true;
                nums[depth] = i;
                dfs(depth+1);
                book[i] =  false;
            }
        }

    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        sum = scanner.nextInt();

        book = new boolean[n+2];
        nums = new int[n+2];

        dfs(1);


    }
}

但是,事情真的那么简单吗?虽说这道题确实过了,但是那只是因为他的数据范围够小,但是我们注意到了,在计算sum的过程中,我们的时间复杂度甚至已经到达了 O ( N 2 ) O(N^2) O(N2),这个复杂度在其他题目中是很危险的,我们得优化一下

我们先看看题目中样例:

3 1 2 4

4 3 6

7 9

16

我们逐层分解,4由3 1组成 3由1 2 组成。。。

3 1 2 4

3 1 1 2 2 4

3 1 1 2 1 2 2 4

经过上面的分解,是否感觉找到了一点规律?没错,除了最左边和最右边的数字以外,中间的所有数字每多一层就会对整体的sum多一次贡献,1 对第一层贡献了1次,对第二层贡献了两次,对第三层贡献了三次。。。。但是第四层是没有重复贡献的,再画一下图,左边表示每一层的数字序列,右边表示初始数字参与的贡献

3 1 2 4 (1,1,1,1)

4 3 6 (1,2,2,1)

7 9 (1,3,3,1)

16 (1,3,3,1)

等等,是不是有点熟悉?高中排列组合?

(1,3,3,1) = ( C 3 0 , C 3 1 , C 3 2 , C 3 3 ) (C_3^0,C_3^1,C_3^2,C_3^3) (C30,C31,C32,C33)

那么,这不就出来了吗?

其实也好理解,数字三角形的数学知识基本上跟排列组合脱不了干系

那么现在我们可以推出公式:
s u m = ∑ i = 0 n ( C n i ∗ a [ i ] ) sum = \sum_{i=0}^{n}(C_n^i*a[i]) sum=i=0n(Cnia[i])
所以接下来代码就好写了,我们只需要在递归外部求出组合数的值就能快速解决问题,大大降低了时间复杂度

package com.lanqiao;

import java.util.Arrays;
import java.util.Scanner;

/**
 * @author 王宇哲
 */
public class 算法训练_数字游戏2 {
    static int n;
    static int sum;

    /**
     * 标记是否已经找到满足条件的排列方式
     */
    static boolean hasFounded = false;

    /**
     * 标记该点是否访问过
     */
    static boolean[] book;

    /**
     * 存储排列数的容器
     */
    static int[] nums;

    static int[][] C;

    /**
     * 深搜模板
     * @param depth 搜索的深度,初始为1,0 深度留空,为了给求和留出位置
     */
    static void dfs(int depth){

        //如果已经找到了排列,就不运行了
        if (hasFounded){
            return;
        }

        //如果已经枚举到了最后一位,就开始判断排列是否满足条件
        if(depth == n + 1){

            int currentSum = 0;

            for(int i=1;i<=n;i++){
                currentSum += C[i-1][n-1]*nums[i];
            }

            //如果满足条件直接输出
            if(currentSum == sum){
                for(int i=1;i<=n;i++){
                    System.out.print(nums[i]+" ");
                }

                //标记找到
                hasFounded = true;
            }
            return;
        }

        for(int i = 1;i<=n;i++){
            //该点未被访问而且排列尚未找到,就继续运行
            if(!book[i] && !hasFounded){

                //深搜模板
                book[i] = true;
                nums[depth] = i;
                dfs(depth+1);
                book[i] =  false;
            }
        }

    }

    /**
     * 获取组合数
     */
    static void getC(){

        //0的情况要特判,从0个数中取出0个数只有一种取法
        for(int i=0;i<=n;i++){
            C[0][i]=C[i][i]=1;
        }
        for(int i=2;i<=n;i++){
            for(int j=1;j<i;j++){
                C[j][i] = C[j-1][i-1]+C[j][i-1];
            }
        }

    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        sum = scanner.nextInt();

        book = new boolean[n+2];
        nums = new int[n+2];

        //初始化排列组合数
        C = new int[n+1][n+1];

        //计算组合数
        getC();

        dfs(1);


    }
}

组合数

顺便说一下组合数的求法,其实大家仔细看的话,会发现组合数其实就是杨辉三角形,组合数其实含义就是从n个物品中取出m个物品的取法种数,所以是可以使用动态规划求解的

我们从n个东西中取出m个东西,假设当前我们之前已经从n-1个东西中成功取出了m个东西,那么当前我就不需要取了,但是假设我们从n-1个东西中上次只取出了m-1个东西,那么当前我需要再取一个才能满足 C n m C_n^m Cnm,所以从n个东西中取出m个东西的方案总数就是从从状态n-1对于取与不取的纠结中得来的,因此我们可以得到递推式:
C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1
over~

你可能感兴趣的:(蓝桥杯,算法,蓝桥杯,动态规划,概率论)