最小化差题目(最接近目标值的子序列和、将数组分成两个数组并最小化数组和的差 最后一块石头的重量 II)

最小化差题目

折半枚举 + 二分查找

总和体积小的话,可以转为01包问题

DP

1755. 最接近目标值的子序列和

题意

给你一个整数数组 nums 和一个目标值 goal 。

你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和为 sum ,你需要 最小化绝对差 abs(sum - goal)

题解

(折半枚举,二分查找) \(O\left(n 2^ \frac{n}{2}\right)\),

  1. 枚举前半个数组所有组合的值,记录在数组 \(q\)中。
  2. \(q\) 数组从小到大排序。
  3. 枚举另一半数组,对于某个组合 \(t\), 在另一个数组中二分查找 goal \(-t\) 。找到第一个大于等于 \(g o a l-t\) 的位置 \(l\), 用 \(l\)\(l-1\) 两个位置的元蛪更新答案。

时间复杂度

  • 预处理并排序 \(h\) 数组的时间为 \(O\left(2^{\frac{n}{2}} \log \left(2^{\frac{n}{2}}\right)\right)=O\left(n 2^{\frac{n}{2}}\right)\) \(\$_{0}\)
  • 对于另一半数组的每个组合,需要 \(O\left(\log \left(2^{\frac{n}{2}}\right)\right)\) 的时间二分 查询。
  • 故总时间复杂度为 \(O\left(n 2^{\frac{n}{2}}\right)\)

枚举可以分为二进制枚举和dfs

二进制枚举子集写法

class Solution {
public:
    int minAbsDifference(vector<int>& nums, int goal) {
        const int n = nums.size();
        const int m = n >> 1;

        vector<int> h;
        for (int s = 0; s < (1 << m); s++) {
            int t = 0;
            for (int i = 0; i < m; i++)
                if (s & (1 << i))
                    t += nums[i];
            h.push_back(t);
        }
        sort(h.begin(), h.end());

        int ans = INT_MAX;
        for (int s = 0; s < (1 << (n - m)); s++) {
            int t = goal;
            for (int i = m; i < n; i++)
                if (s & (1 << (i - m)))
                    t -= nums[i];

            int l = 0, r = h.size() - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (h[mid] < t) l = mid + 1;
                else r = mid;
            }

            if (ans > abs(h[l] - t))
                ans = abs(h[l] - t);

            if (l < h.size() - 1 && ans > abs(h[l + 1] - t))
                ans = abs(h[l + 1] - t);
        }

        return ans;
    }
};

dfs写法

const int N = 1100010;
int q[N];
class Solution {
public:
    int n, cnt, goal, ans;
    void dfs1(vector<int>& nums, int u, int s)
    {
        if(u == n / 2)
        {
            q[cnt ++] = s;   
            return ;
        }
        dfs1(nums, u + 1, s);
        dfs1(nums, u + 1, s + nums[u]);
    }
    void dfs2(vector<int>& nums, int u, int s)
    {
        if(u == n)
        {
            int l = 0, r = cnt - 1;
            while(l < r)
            {
                int mid = l + r + 1 >> 1;
                if(q[mid] + s <= goal) l = mid;
                else r = mid - 1;
            }
            ans = min(ans, abs(q[l] + s - goal));
            if(l + 1 < cnt)
                ans = min(ans, abs(q[l + 1] + s - goal));
            return ;
        }
        dfs2(nums, u + 1, s);
        dfs2(nums, u + 1,s + nums[u]);
    }
    int minAbsDifference(vector<int>& nums, int _goal) {
        n = nums.size(), goal = _goal, cnt = 0, ans = INT_MAX;
        dfs1(nums, 0, 0);
        sort(q, q + cnt);
        dfs2(nums, n / 2, 0);

        return ans;
    }
};

2035. 将数组分成两个数组并最小化数组和的差

题意

给你一个长度为 2 * n 的整数数组。你需要将 nums 分成 两个 长度为 n 的数组,分别求出两个数组的和,并 最小化 两个数组和之 差的绝对值 。nums 中每个元素都需要放入两个数组之一。

请你返回 最小 的数组和之差。

题解

根上题一样,对于前n个数,采用二进制或者dfs枚举,开一个二维的vector保存,即q[cnt],表示前n个数选取cnt个数和的所有结果。后n个数,采用二进制或者dfs枚举,假设枚举选取了cnt1个数,在q[n - cnt1]二分选取结果即可。

dfs写法

class Solution {
public:
    int s = 0, res = INT_MAX;
    vector<vector<int> > q;
    void dfs1(vector<int>& nums, int u, int sum, int num){
        if(u == (nums.size()>>1)){
            q[num].push_back(sum);
            return;
        }
        dfs1(nums, u + 1, sum, num);
        dfs1(nums, u + 1, sum + nums[u], num + 1);
    }
    void dfs2(vector<int>& nums, int u, int sum, int num){
        if(u == nums.size()){
            
            int cnt = nums.size() / 2 - num;
            int l = 0, r = q[cnt].size() - 1;
            while(l < r){
                int mid = (l + r) >> 1;
                if(sum + q[cnt][mid] >= s/2) r = mid;
                else l = mid + 1;
            }
           // cout<<l<<" "<<cnt<<" "<<num<<" "<<q[cnt][l]<<" "<<sum<<endl;
            if(l >= 0 && l < q[cnt].size())res = min(res, abs(s - 2 * (sum + q[cnt][l])));
            if(l >= 1) res = min(res, abs(s - 2 * (sum + q[cnt][l - 1])));
            return;
        }
        dfs2(nums, u + 1, sum, num);
        dfs2(nums, u + 1, sum + nums[u], num + 1);
    }
    int minimumDifference(vector<int>& nums) {
        for(auto x : nums) s += x;
        int n = nums.size() /2;
        q = vector<vector<int> >(n + 1);
        dfs1(nums, 0, 0, 0);
        for(int i = 1; i <= n; i++)sort(q[i].begin(), q[i].end());
        dfs2(nums, n, 0, 0);
        return res;
    }
};

二进制枚举子集写法

class Solution {
public:
    int minimumDifference(vector<int>& nums) {
        int n = nums.size();
        n /= 2;
        vector<vector<int>> q(n+1);
        for(int i = 0; i < 1 << n; i++){
            int cnt = 0, sum = 0;
            int S = 0;
            for(int j = 0; j < n; j++) S += nums[j+n];
            for(int j = 0; j < n; j++){
                if(i >> j & 1){
                    cnt ++ ;
                    sum += nums[j+n];
                }
            }
            q[cnt].push_back(sum - (S-sum));
        }
        int ans = 1e9;
        for(int i = 0; i <= n; i++) sort(q[i].begin(), q[i].end());
        for(int i = 0; i < 1 << n; i++){
            int cnt = 0, sum = 0;
            int S = 0;
            for(int j = 0; j < n; j++) S += nums[j];
            for(int j = 0; j < n; j++){
                if(i >> j & 1){
                    cnt ++ ;
                    sum += nums[j];
                }
            }
            cnt = n - cnt;
            sum = (S-sum)- sum;
            auto t = lower_bound(q[cnt].begin(), q[cnt].end(), sum);
            if(t != q[cnt].end()) ans = min(ans, abs(sum - *t));
            if(t != q[cnt].begin()) ans = min(ans, abs(sum - *prev(t)));
        }
        return ans;
    }
};

1049. 最后一块石头的重量 II

题意

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

题解

可以分析到:

可以在纸上模拟一下,就会发现最后的结果可以表示为:

\[\sum_{i=0}^{n-1} k_{i} \times \text { stonesi }, \quad k_{i} \in\{-1,1\} \]

转化为该题494. 目标和从 stones数组中选择,凑成总和不超过sum/2的最大价值。

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int n = stones.size();
        vector<int> f(30010, 0);
        int sum = 0;
        for(int x : stones) sum += x;
        int m = sum / 2;
        for(int i = 0; i < n; i++){
            for(int j = m; j >= stones[i]; j--){
                f[j] = max(f[j], f[j - stones[i]] + stones[i]);
            }
        }
        return sum - f[m] -f[m];
    }
};
posted @ 2021-10-12 15:19  pxlsdz  阅读(433)  评论(0编辑  收藏  举报