【算法题】牛牛的背包问题(分治)

牛牛的背包问题_网易笔试题_牛客网 (nowcoder.com)

一、题目

牛牛准备参加学校组织的春游, 出发前牛牛准备往背包里装入一些零食, 牛牛的背包容量为w。
牛牛家里一共有n袋零食, 第i袋零食体积为v[i]。
牛牛想知道在总体积不超过背包容量的情况下,他一共有多少种零食放法(总体积为0也算一种放法)。

输入描述:
输入包括两行
第一行为两个正整数n和w(1 <= n <= 30, 1 <= w <= 2 * 10^9),表示零食的数量和背包的容量。
第二行n个正整数v[i](0 <= v[i] <= 10^9),表示每袋零食的体积。

输出描述:
输出一个正整数, 表示牛牛一共有多少种零食放法。

输入

3 10
1 2 4

输出

8

说明

三种零食总体积小于10,于是每种零食有放入和不放入两种情况,一共有2*2*2 = 8种情况。

二、代码

import java.util.Map.Entry;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.TreeMap;
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            int n = (int) in.nval;
            in.nextToken();
            int bag = (int) in.nval;
            int[] arr = new int[n];
            for (int i = 0; i < n; i++) {
                in.nextToken();
                arr[i] = (int) in.nval;
            }
            long ways = ways(arr, bag);
            out.println(ways);
            out.flush();
        }
    }

    public static long ways(int[] arr, int bag) {
        // 过滤简单参数
        if (arr == null || arr.length == 0) {
			return 0;
		}
		if (arr.length == 1) {
			return arr[0] <= bag ? 2 : 1;
		}

        // 这道题需要用到分治,将整个大数组左右分成两个部分
        // 记录左部分能够实现的放零食的方法  key:表示包内正好放多大体积的零食  value:正好放满key体积的零食一共有多少种方法
        TreeMap<Long, Long> lmap = new TreeMap<Long, Long>();
        // 记录又部分能够实现的放零食的方法  key:表示包内正好放多大体积的零食  value:正好放满key体积的零食一共有多少种方法
        TreeMap<Long, Long> rmap = new TreeMap<Long, Long>();
        // 将数组评分
        int mid = (arr.length - 1) >> 1;
        // 暴力递归,返回值是整个左部分全部的全部不超过bag大小的放零食方法数
        long leftWays = process(arr, 0, mid, 0L, bag, lmap);
        // 暴力递归,返回值是整个右部分全部的全部不超过bag大小的放零食方法数
        long rightWays = process(arr, mid + 1, arr.length - 1, 0L, bag, rmap);

        // 这里取一个右部份的前缀方法数,用来和左部分组合,整合出中间部分的方法数
        // (key, value):表示包内装零食体积小于等于key的方法总数value
        TreeMap<Long, Long> preRightMap =  new TreeMap<Long, Long>();
        Long pre = 0L;
        // 生成右部分的前缀方法数
        for (Entry<Long, Long> entry : rmap.entrySet()) {
            // 将方法数进行前缀累加,TreeMap会自动根据key从小到大排序
            pre += entry.getValue();
            preRightMap.put(entry.getKey(), pre);
        }

        // 记录从中间部分选的方法总数
        long midWays = 0L;
        // 遍历左部分的方法数,以每一个左部分方法为基准,凑一个右部分最合适的方法数,来整合出中间部分的方法数
        for (Entry<Long, Long> entryL : lmap.entrySet()) {
            // floorKey(K key)	返回小于或等于给定键的最大键,如果没有这样的键,则null
            // 找到当前左部分的体积 +  右部份的体积   小于等于bag的方法数最多的那一个选择
            Long preRightKey = preRightMap.floorKey(bag - entryL.getKey());
            if (preRightKey != null) {
                // 两个方法数相乘,记录中间部分的方法数
                midWays += (preRightMap.get(preRightKey) * entryL.getValue());
            }
        }
        // 将左部分,右部份,中间部分的方法数相加,还要加1,是指什么都不选,因为在暴力递归,并没有将什么都不选算成是一种方法,为了避免在整合期间多算一个方法。这里我们就在最后手动加上
        return leftWays + rightWays + midWays + 1;

    }
    public static long process(int[] arr, int index, int end, long sumV, int bag, TreeMap<Long, Long> map) {
        // 递归出口,当前放零食的总体积大于bag,背包放不下了,所以这种放法不成立,返回0
        if (sumV > bag) {
            return 0;
        }
        // 遍历完了所有的零食,并且sumV没有超过bag,说明这是一种合法的方法
        if (index > end) {
            // sumV != 0说明有选择零食
            if (sumV != 0) {
                // 如果这个选择的体积在map中已经存在了,就将这个体积的方法数+1
                if (map.containsKey(sumV)) {
                    map.put(sumV, map.get(sumV) + 1L);
                // 如果是第一次凑出这个体积,就创建这个key-value对
                } else {
                    map.put(sumV, 1L);
                }
                // 返回方法数1,用来在递归过程中统计整个index~end范围的总方法数
                return 1;
            // 如果没有选择零食,这里返回0,不把他算成一种方法,这是为了避免在后续左右部分整合过程中算重,比如左边什么都不选算是一种方法,右边什么都不选也算一种方法,左右整合就变成两种的方法了,但实际还应该是一种方法
            } else {
                return 0;
            }
            
        }

        // 两种选择,index位置的零食要或者不要
        long ways = process(arr, index + 1, end, sumV + arr[index], bag, map); 
        ways += process(arr, index + 1, end, sumV, bag, map); 
        // 返回左右递归分支总方法数
        return ways;
    }
}

三、解题思路 

这个题的零食体积非常大,如果用常规的动态规划求解模型,会超时。但是这道题目对零食数组的大小规定的很小,这就让我们可以直接利用分治+暴力枚举在不超时的情况下通过。

整体思路就是先求左部分的所有方法数,再求右部分的所有方法数,再用左右两部分整合出中间部分的所有方法数,所得到的三个部分的方法数总和就是最终结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值