题目列表
6354. K 件物品的最大和(easy)
6355. 质数减法运算(medium)
6357. 使数组元素全部相等的最少操作次数(medium)
6356. 收集树中金币(hard)
直接模拟,优先级为: + 1 > + 0 > − 1 +1>+0>-1 +1>+0>−1
class Solution {
public:
int kItemsWithMaximumSum(int numOnes, int numZeros, int numNegOnes, int k) {
if(k<=numOnes)return k;
else if(k<=numOnes+numZeros){//数量不够需要0凑数
return numOnes;
}
else{
k-=numOnes+numZeros;//还不够,不得不扣
return numOnes-k;
}
}
};
从高到低,如果遇到 n u m s [ i ] > = n u m s [ i + 1 ] nums[i]>=nums[i+1] nums[i]>=nums[i+1] ,则寻找一个最小的质数 q q q ,使得 n u m s [ i ] − q < n u m s [ i + 1 ] nums[i]-q
以为1是质数,WA了一次。小学数学老师气死。
class Solution {
public:
bool isPrime(int n){
if(n==1) return false;
if(n==2)return true;
for(int i=2;i<=sqrt(n);i++){
if(n%i==0)return false;
}
return true;
}
bool primeSubOperation(vector<int>& nums) {
for(int i=nums.size()-2;i>=0;i--){
if(nums[i]>=nums[i+1]){
bool flag=false;
for(int j=1;j<nums[i];j++){
if(isPrime(j)){
if(nums[i]-j<nums[i+1]){
nums[i]-=j;
flag=true;
break;
}
}
}
if(!flag) return flag;
}
}
for(auto num:nums) cout<<num<<" ";
cout<<endl;
return true;
}
};
数据范围是 1 0 5 10^5 105,暴力必超。但是不信邪,也找不到更好的办法了,头铁了一下,贡献了一个WA。 O ( n ) O(n) O(n)时间也是不可能的,所以这题的优化肯定在 O ( n l o g n ) O(nlogn) O(nlogn) 上。提到 O ( n l o g n ) O(nlogn) O(nlogn),那必然就涉及排序、二分等等。这题也确实是这个方向。
观察可以发现,每次大于 q u e r i e s [ i ] queries[i] queries[i] 的数字得往下减,小于的得往上加。那能不能不一个一个减,一次就把小于 q u e r i e s [ i ] queries[i] queries[i] 的数拉到等于 q u e r i e s [ i ] queries[i] queries[i] ,再一次性把大于 q u e r i e s [ i ] queries[i] queries[i] 的数拉到等于 q u e r i e s [ i ] queries[i] queries[i] 。当然可以,假设小于 q u e r i e s [ i ] queries[i] queries[i] 的数一共有 n n n 个,且这 n n n 个数的和为 s u m sum sum ,则 + 1 +1 +1 的操作次数为 q u e r i e s [ i ] ∗ n − s u m queries[i]*n-sum queries[i]∗n−sum ,大于的部分同理。如果这样做的话,我们每次需要分别统计大于和小于 q u e r i e s [ i ] queries[i] queries[i] 的数的个数,还得分别算他们的和,貌似并没有优化。但是如果我们把原数组排序,并且计算一个前缀和,那我们可以用二分查找,查找 q u e r i e s [ i ] queries[i] queries[i] 在排序后的数组里面的位置,就可以一次性得到上述所需的四个值。
class Solution {
public:
vector<long long> minOperations(vector<int>& nums, vector<int>& queries) {
int m=queries.size();
int n=nums.size();
vector<long long> res(m,0);
vector<long long> sum(n+1,0);
sort(nums.begin(),nums.end());
for(int i=1;i<=n;i++){
sum[i]+=sum[i-1]+nums[i-1];
}
for(int i=0;i<m;i++){
long long q=queries[i];
int idx=upper_bound(nums.begin(),nums.end(),q)-nums.begin();
// cout<
res[i]+=sum[n]-sum[idx]-q*(n-idx)+(idx)*q-sum[idx];
}
return res;
}
};
这种题再给我十个脑子我也想不出来,像极了数学证明题。看着答案都要想半天才知道为什么要这么做。
先把没金币的子树删掉,因为没必要走过去。此时剩下的拓扑就是都得去走一遍的;然后我们从叶子节点往上走一次,并记录每个点进队的时间。由于是从叶子倒着走的,所以其实这个时间就代表了该点到叶子节点的距离。神奇之处在于,这么走一遍之后,所有两端节点的时间大于2的边,都是需要经过的边,且由于需要回到起点,经过次数为两次。换个说法就是,没有任何一条边需要走三次,且每一条边都要走两次!我也很Amazing。因为主干道上的金币在走的过程中顺路收集,而叶子节点不用走到头,所以其实考虑走到何处可以拿到叶子节点上的金币,并在此处停下掉头就行了。至于为什么不会有经过超过两次的边,其实知道这个结论之后去举例子,发现确实很容易证明,难点在于怎么想得到这个约束条件呢。
class Solution {
public:
int collectTheCoins(vector<int>& coins, vector<vector<int>>& edges) {
int n=coins.size();
vector<vector<int>> g(n);//邻接表
vector<int> deg(n,0);//每个节点的度
for(auto e:edges){
int x=e[0],y=e[1];
g[x].push_back(y);
g[y].push_back(x);
deg[x]++;
deg[y]++;
}
//删掉无金币的子树
queue<int> q;
for(int i=0;i<n;i++){
if(deg[i]==1&&coins[i]==0){//度为一,即叶子节点,且无金币
q.push(i);
}
}
while(!q.empty()){
int x=q.front();
q.pop();
for(auto y:g[x]){
//由于队里都是无金币叶子,故y就是与其相连的唯一的一个节点,也就是其父亲
//如果删掉当前叶子之后父亲的度为1且父亲也没金币,那父亲也变成了无金币叶子,应该在下一轮被删掉,故入队
//而且由于第二次访问的时候也是倒着走的,且留下的都是有金币的叶子,我们直接度减一就可以做到删掉该节点,而无需去删对应的边
if(--deg[y]==1&&coins[y]==0){
q.push(y);
}
}
}
//现在只剩下有金币叶子及其路径了,再走一遍,计算每个节点到叶子的距离
for(int i=0;i<n;i++){
if(deg[i]==1&&coins[i]){
q.push(i);
}
}
if(q.size()<=1) return 0;//如果只有一个叶子有金币,或完全无金币,则不需要拿金币或站在起点就能拿到金币
vector<int> time(n,0);
while(!q.empty()){
int x=q.front();
q.pop();
for(auto y:g[x]){
if(--deg[y]==1){//只有当孩子全都没了,当前节点是叶子了,才入队并更新时间
time[y]=time[x]+1;
q.push(y);
}
}
}
int res=0;
for(auto e:edges){
int x=e[0],y=e[1];
if(time[x]>=2&&time[y]>=2) res+=2;
}
return res;
}
};