最小化差题目(最接近目标值的子序列和、将数组分成两个数组并最小化数组和的差 最后一块石头的重量 II)
最小化差题目
折半枚举 + 二分查找
总和体积小的话,可以转为01包问题
DP
1755. 最接近目标值的子序列和
题意
给你一个整数数组 nums 和一个目标值 goal 。
你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和为 sum ,你需要 最小化绝对差 abs(sum - goal)
题解
(折半枚举,二分查找) \(O\left(n 2^ \frac{n}{2}\right)\),
- 枚举前半个数组所有组合的值,记录在数组 \(q\)中。
- 将 \(q\) 数组从小到大排序。
- 枚举另一半数组,对于某个组合 \(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。
题解
可以分析到:
可以在纸上模拟一下,就会发现最后的结果可以表示为:
转化为该题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];
}
};