折半枚举算法详解

282 阅读2分钟

前言

讲这个之前,先举几个例子以及一些解决方法。 比如:一个长度为1000的数组中,我想要从数组中分成两个数组(不要求两个数组大小相同),使得两个数 组的绝对值之差最小。 这道题在不要求数组中存储值的范围是很难做的,如果范围很大,那么只能依靠dfs暴力搜索答案,哪怕是 加上一些剪枝优化,复杂度也是相当的惊人。 如果范围很小,比如 sum/2 在10^4左右,这个问题还是很好解的,这就是很常见的背包问题 伪代码:

for i in nums
    for j=sum/2, j>nums[i], j--
        dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);

很显然答案是 abs(sum - 2*dp[sum/2]);

再比如:我有一个长度为16的数组,但是我数组中存储的值特别大,能不能解? 当然这非常好解,为什么? 因为它的长度才16,2^16次方才多大,轻轻松松的暴力即可。

进入正题:

如果我有一个长度为32的数组呢,能解么?
这个时候估计就有些傻眼了,2^32次方这么高的复杂度,这谁能抗的住。
这个时候我们想到,能把数组分成两个子数组分别枚举么?
好的,有了这个想法,那具体怎么做呢?
一步一步来
    我们能够得到什么?
        我们能分别二进制枚举两个子数组,拿到所有的结果,并用两个集合存储下来。
    我们要求解什么?
        获取数组中一些元素减去另一些元素的绝对值差最小。
    从我们得到的与想要的结果进行问题演化。
        答案为min(abs(oneResults[x] + twoResults[y])), 这个答案越接近0是不是越符合答
        案,哇塞,现在是否看到了解决这个问题的希望。
        对twoResults集合进行排序,遍历oneResults,每次二分去twoResults中查找。
        最后取最小值就是答案。
    最后我们来分析一下复杂度
        一开始,我们是要2^32复杂度,现在呢?
        大概是2 * O(2^16) + 2 * O(2^16) * O(16)

程序实现

/**
 * 最后在来一道例题
 * 将nums数组分成两个大小相等数组,要求两个数组的绝对值之差最小
 * 数据范围: 1 <= n <= 15; nums = 2 * n; -10^7 <= nums[i] <= 10^7
 * 解题思路: 将问题转化成对半暴力枚举,也就是将数组分成左右两边,分别二进制枚举,保存取x个整数
 *          会有结果集,然后对其中一个存储的集合进行排序便于二分查找
 * @param nums 传入的数组
 * @return  返回最后的结果
 */
public static int minimumDifference(int[] nums) {
    int n = nums.length / 2, sum = 0;
    int[] a = new int[n];
    int[] b = new int[n];
    /**
     * oneMap/twoMap: 用于保存左/右半边的结果集
     * key: 整数数量 
     * value: 存储左/右半边取key个正整数加起来的和有多少种结果
     */
    Map<Integer, List<Integer>> oneMap = new HashMap<>();
    Map<Integer, List<Integer>> twoMap = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        sum += nums[i];
        if (i < n) {
            a[i] = nums[i];
            oneMap.put(i, new ArrayList<>());
            continue;
        }
        b[i - n] = nums[i];
        twoMap.put(i - n, new ArrayList<>());
    }
    oneMap.put(n, new ArrayList<>());
    twoMap.put(n, new ArrayList<>());
    for (int i = 0; i < (1 << n); i++) {
        int cnt = 0, ansOne = 0, ansTwo = 0;
        for (int j = 0; j < n; j++) {
            if (((1 << j) & i) > 0) {
                ansOne += a[j];
                ansTwo += b[j];
                cnt++;
            }
        }
        oneMap.getOrDefault(cnt, new ArrayList<>()).add(ansOne);
        twoMap.getOrDefault(cnt, new ArrayList<>()).add(ansTwo);
    }
    twoMap.forEach((key, value) -> twoMap.put(key, value.stream().sorted().collect(Collectors.toList())));
    // oneMap.forEach((key, value) -> System.out.println("key: " + key + " value: " + value));
    // twoMap.forEach((key, value) -> System.out.println("key: " + key + " value: " + value));
    AtomicInteger ans = new AtomicInteger(Integer.MAX_VALUE);
    for (int i = 0; i <= n; i++) {
        int finalI = i;
        int finalSum = sum;
        oneMap.get(i).forEach(bean -> {
            int two = (finalSum / 2) - bean;
            List<Integer> twoList = twoMap.get(n - finalI);
            int l = 0, r = twoList.size() - 1, pos = twoList.size();
            while (l <= r) {
                int mid = (l + r) >> 1;
                if (twoList.get(mid) > two) {
                    pos = mid;
                    r = mid - 1;
                    continue;
                }
                l = mid + 1;
            }
            if (pos != twoList.size()) {
                ans.set(Math.min(ans.get(), Math.abs(finalSum - 2 * (bean + twoList.get(pos)))));
            }
            if (pos > 0) {
                ans.set(Math.min(ans.get(), Math.abs(finalSum - 2 * (bean + twoList.get(pos - 1)))));
            }
        });
    }
    return ans.get();
}