本文涉及知识点
LeetCode 2835. 使子序列的和等于目标的最少操作次数
给你一个下标从 0 开始的数组 nums ,它包含 非负 整数,且全部为 2 的幂,同时给你一个整数 target 。
一次操作中,你必须对数组做以下修改:
选择数组中一个元素 nums[i] ,满足 nums[i] > 1 。
将 nums[i] 从数组中删除。
在 nums 的 末尾 添加 两个 数,值都为 nums[i] / 2 。
你的目标是让 nums 的一个 子序列 的元素和等于 target ,请你返回达成这一目标的 最少操作次数 。如果无法得到这样的子序列,请你返回 -1 。
数组中一个 子序列 是通过删除原数组中一些元素,并且不改变剩余元素顺序得到的剩余数组。
示例 1:
输入:nums = [1,2,8], target = 7
输出:1
解释:第一次操作中,我们选择元素 nums[2] 。数组变为 nums = [1,2,4,4] 。
这时候,nums 包含子序列 [1,2,4] ,和为 7 。
无法通过更少的操作得到和为 7 的子序列。
示例 2:
输入:nums = [1,32,1,2], target = 12
输出:2
解释:第一次操作中,我们选择元素 nums[1] 。数组变为 nums = [1,1,2,16,16] 。
第二次操作中,我们选择元素 nums[3] 。数组变为 nums = [1,1,2,16,8,8] 。
这时候,nums 包含子序列 [1,1,2,8] ,和为 12 。
无法通过更少的操作得到和为 12 的子序列。
示例 3:
输入:nums = [1,32,1], target = 35
输出:-1
解释:无法得到和为 35 的子序列。
提示:
1 <= nums.length <= 1000
1 <= nums[i] <= 230
nums 只包含非负整数,且均为 2 的幂。
1 <= target < 231
位运算
target可以拆分成 2i1+2i2+
⋯
\cdots
⋯ + 2 in
性质一:如果2j1+2j2
…
\dots
…+2jm >= 2i 且 j1到jm都小于等于i。
则一定可以从 j1,j2
⋯
\cdots
⋯jm 中选择若干数,使得其和等于2i。
证明:
i = 0 时 。 20=20.
x>=1,如果i=x,性质一成立,则i=x+1,性质一也成立。
由于左式 >= 2x+1 > 2x 故左式可以抽取s = 2x
左式 - S >= 2x,故还可以抽取S2 = 2x
S+S2和在一起,就是2x+1
性质二:
令集合 T = {2j1,2j2
⋯
\cdots
⋯, 2jm} , T 中可能有重复的数据。
令集合S ={ 2i1,2i2+
⋯
\cdots
⋯ , 2in },其中i1<i2 <
⋯
\cdots
⋯ in S的和等于target
∀
i
(
i
∈
i
1
,
i
2
,
⋯
,
i
n
)
t
a
r
g
e
t
i
=
∑
x
<
=
2
i
,
x
∈
S
T
i
=
∑
x
<
=
2
i
x
,
x
∈
T
\forall i(i \in{i1,i2,\cdots,in}) \quad targeti = \sum_{x <= 2^i },x\in S \quad Ti=\sum_{x <= 2^i }x,x\in T
∀i(i∈i1,i2,⋯,in)targeti=x<=2i∑,x∈STi=x<=2i∑x,x∈T
如果Ti大于等于targeti
⟺
\iff
⟺ 本题
情况一:target只有一项,就是性质一。
情况二:如果target有x项成立,则x+1项也成立。移除x项后,就成了性质一。
解法
通过i从低位到高位枚举target,其和记录到:iNeed。
cur = 1 << i 。
nums中小于等于cur的加到llHas中。
如果 llHas < iNeed, 则拆分nums中的最小元素next到cur,如果无元素可拆分,则返回-1。
拆分后:一个cur加到llHas, next/2 next/4
…
\dots
… cur 加到setNum。
本解法用的多键集合,其实用大根堆 更简洁。
代码
核心代码
class Solution {
public:
int minOperations(vector<int>& nums, int target) {
std::multiset<int> setNum(nums.begin(), nums.end());
int iRet = 0;
long long llHas = 0;
int iNeed = 0;
for (int i = 0; i <= 30; i++) {
const int cur = 1 << i;
while (setNum.size() && (*setNum.begin() <= cur)) {
llHas += *setNum.begin();
setNum.erase(setNum.begin());
}
if (cur & target) {
iNeed += cur;
}
while (llHas < iNeed) {
auto it = setNum.lower_bound(cur);
if (setNum.end() == it) { return -1; }
int next = *it;
setNum.erase(it);
while (cur != next) {
next /= 2;
setNum.emplace(next);
iRet++;
}
llHas += cur;
}
}
return iRet;
}
};
测试用例
template<class T>
void Assert(const T& t1, const T& t2)
{
assert(t1 == t2);
}
template<class T>
void Assert(const vector<T>& v1, const vector<T>& v2)
{
if (v1.size() != v2.size())
{
assert(false);
return;
}
for (int i = 0; i < v1.size(); i++)
{
Assert(v1[i], v2[i]);
}
}
int main()
{
vector<int> nums; int target;
{
Solution sln;
nums = { 1, 2, 8 }, target = 7;
auto res = sln.minOperations(nums, target);
Assert(1, res);
}
{
Solution sln;
nums = { 1, 32, 1, 2 }, target = 12;
auto res = sln.minOperations(nums, target);
Assert(2, res);
}
{
Solution sln;
nums = { 1,32,1 }, target = 35;
auto res = sln.minOperations(nums, target);
Assert(-1, res);
}
}
扩展阅读
视频课程
有效学习:明确的目标 及时的反馈 拉伸区(难度合适),可以先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771
如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176
相关下载
想高屋建瓴的学习算法,请下载《喜缺全书算法册》doc版
https://download.csdn.net/download/he_zhidan/88348653
我想对大家说的话 |
---|
闻缺陷则喜是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。 |
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。 |
如果程序是一条龙,那算法就是他的是睛 |
测试环境
操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。