蓝桥杯java备考Day1——无聊的逗

无聊的豆

问题描述

问题描述: 逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的情况下长度最长是多少。
输入格式: 第一行一个数n,表示n个棍子;
第二行n个数,表示各个棍子的长度。
输出格式: 最大的长度。

问题解决思路

问题条件

  • 棍子并不一定都要使用,可以舍弃一部分以达目的;
  • 棍子不可切割;

问题转化

  • 该问题相当于一个整数集合,现要找这个集合的两个子集,要求这两个子集的元素和相等,要求输出符合要求的最大子集元素和。

解决思路

  • 从结果反推,最后的两个子集和肯定是偶数
  • 如果原整数集合的元素和sum是偶数,那么如果其有个子集的元素和为(sum/2),则结果就是(sum/2)
  • 如果没有一个子集的元素和为(sum/2),那么就选择去掉一些元素重复上述步骤。
  • 如果原整数集合的元素和sum是奇数,那么必要去掉一些元素,使得元素和为偶数,然后重复步骤2、3.
    根据上面的大致想法,需要解决的问题:
  • 先将原数组list降序排序,从大的值搜索,可以较快找到结果。
Collections.sort(list, Collections.reverseOrder());
  • 如何判断是否有子集符合条件(子集和为我们想要的目标值(sum/2))?
    • 用01背包
    • 具体实现:创建二维数组bp[][],boolean类型,行数为list的个数,列数为(sum/2+1),即从0到(sum/2) ,bp[i][j]=true,表示数组中i元素或i元素之前的元素之和可以达到j,当第(sum/2)列有true,则表示存在符合条件子集。
    public static boolean canPartition(List<Integer> list){
        //求木棍和
        int sum = list.stream().reduce(Integer::sum).orElse(0);
        //判断全部木棍和是否为偶数
        if(sum%2!=0){
            return false;
        }
        //全部木棍之和是偶数,判断木棍是否可以分为两个等和的子集
        int target = sum/2;
        boolean[][] dp = new boolean[list.size()][];
        for(int i=0;i<dp.length;i++){
            dp[i] = new boolean[target+1];
        }
        if(list.get(0)<=target){
            dp[0][list.get(0)] = true;
        }
        for(int i=1;i<list.size();i++){
            for(int j=0;j<target+1;j++){
                dp[i][j] = dp[i - 1][j];
                if(list.get(i)<j){
                    dp[i][j] = dp[i - 1][j]||dp[i - 1][j - list.get(i)];
                }else if(list.get(i)==j){
                    dp[i][j] = true;
                }
            }
            if(dp[i][target]){
                return true;
            }
        }
        return dp[list.size()-1][target];
    }
  • 在遇到步骤3、4的情况时,如何选择删掉的元素并重复步骤?
    • DFS深度优先搜索,剪枝(需要全排列或者类似情况时可以使用优先搜索)
    • DFS讲解:DFS原理讲解以及示例
    • 具体实现:list从左到右搜索,依次将元素放入path,path为存放符合条件的列表,result存放path列表元素和,如果当前path的元素和cnt大于result,且cnt为偶数,则判断一下当前path列表中是否有子集元素和为path和的一半,如果是,则更新result为cnt,指针向下移动,向下搜索,回溯时,将当前指针指向的元素移出path,直至最后,result/2就是最长长度。
    public static void dfs(List path,int start,int cnt,int length,List nums){
        for(int i=start;i<length;i++){
            if(i>start&&nums.get(i)==nums.get(i-1)){
                continue;
            }
            path.add(nums.get(i));
            cnt+=(int)nums.get(i);
            if (cnt>result[0]&&cnt%2==0){
                if (canPartition(path)){
                    result[0]= cnt;
                }
            }
            dfs(path,i+1,cnt,length,nums);
            cnt -= (int)nums.get(i);
            path.remove(nums.get(i));
        }
    }

例图

请添加图片描述
请添加图片描述

完整代码

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;

/*问题描述:n个木棍,粘成两根木棍,两根木棍一样成,求最长长度
* 输入格式:n
*         n个数,表示每个棍子长度
* 输出格式:最大长度*/
/*思路分析:
* 两根同样长度的木棍,则代表选用的木棍长度之和为偶数,那么,如果所有木棍的长度为偶数,判断是否可以分为两份,可以直接返回总长度一半即可,如果是奇数,则需舍弃一些木棍。
* 问题转化为:在木棍长度列表中选择全部或其中一些正整数,判断存放这些正整数的木棍列表能否被分割成两个等和的列表子集。最后返回能够被分割列表的最大长度之和的一半。
* 如何判断木棍是否可以拼出总长度的一半?01背包
* 如何在删掉一些木棍的列表中找出能够分成两根等长木棍的最大长度?
* DFS得到木棍列表的每个子集,然后分别判断这个子集能够否分成两根等长的木棍,如果可以,比较得出最大值,最后返回结果,期间需要剪枝。*/
public class Exercise1 {
    public static final int[] result = new int[1];
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<n;i++){
            list.add(scan.nextInt());
        }
        result[0] = -1;
        Collections.sort(list, Collections.reverseOrder());
        if (canPartition(list)){
            int sum = list.stream().reduce(Integer::sum).orElse(0);
            System.out.print(sum/2);
        }else {
            List<Integer> path = new ArrayList<>();
            dfs(path, 0, 0,list.size(),list);
            System.out.print(result[0] / 2);
        }
        scan.close();
    }
    public static boolean canPartition(List<Integer> list){
        //求木棍和
        int sum = list.stream().reduce(Integer::sum).orElse(0);
        //判断全部木棍和是否为偶数
        if(sum%2!=0){
            return false;
        }
        //全部木棍之和是偶数,判断木棍是否可以分为两个等和的子集
        int target = sum/2;
        boolean[][] dp = new boolean[list.size()][];
        for(int i=0;i<dp.length;i++){
            dp[i] = new boolean[target+1];
        }
        if(list.get(0)<=target){
            dp[0][list.get(0)] = true;
        }
        for(int i=1;i<list.size();i++){
            for(int j=0;j<target+1;j++){
                dp[i][j] = dp[i - 1][j];
                if(list.get(i)<j){
                    dp[i][j] = dp[i - 1][j]||dp[i - 1][j - list.get(i)];
                }else if(list.get(i)==j){
                    dp[i][j] = true;
                }
            }
            if(dp[i][target]){
                return true;
            }
        }
        return dp[list.size()-1][target];
    }
    public static void dfs(List path,int start,int cnt,int length,List nums){
        for(int i=start;i<length;i++){
            if(i>start&&nums.get(i)==nums.get(i-1)){
                continue;
            }
            path.add(nums.get(i));
            cnt+=(int)nums.get(i);
            if (cnt>result[0]&&cnt%2==0){
                if (canPartition(path)){
                    result[0]= cnt;
                }
            }
            dfs(path,i+1,cnt,length,nums);
            cnt -= (int)nums.get(i);
            path.remove(nums.get(i));
        }
    }
}

结果

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值