难度简单0
袋子中装有一些物品,每个物品上都标记着数字 1
、0
或 -1
。
给你四个非负整数 numOnes
、numZeros
、numNegOnes
和 k
。
袋子最初包含:
numOnes
件标记为 1
的物品。numZeroes
件标记为 0
的物品。numNegOnes
件标记为 -1
的物品。现计划从这些物品中恰好选出 k
件物品。返回所有可行方案中,物品上所标记数字之和的最大值。
示例 1:
输入:numOnes = 3, numZeros = 2, numNegOnes = 0, k = 2
输出:2
解释:袋子中的物品分别标记为 {1, 1, 1, 0, 0} 。取 2 件标记为 1 的物品,得到的数字之和为 2 。
可以证明 2 是所有可行方案中的最大值。
示例 2:
输入:numOnes = 3, numZeros = 2, numNegOnes = 0, k = 4
输出:3
解释:袋子中的物品分别标记为 {1, 1, 1, 0, 0} 。取 3 件标记为 1 的物品,1 件标记为 0 的物品,得到的数字之和为 3 。
可以证明 3 是所有可行方案中的最大值。
提示:
0 <= numOnes, numZeros, numNegOnes <= 50
0 <= k <= numOnes + numZeros + numNegOnes
class Solution {
public int kItemsWithMaximumSum(int numOnes, int numZeros, int numNegOnes, int k) {
if(k <= numOnes) return k;
else if(k <= numOnes + numZeros) return numOnes;
else return numOnes - (k - numOnes - numZeros);// 7-5-1
}
}
难度中等2
给你一个下标从 0 开始的整数数组 nums
,数组长度为 n
。
你可以执行无限次下述运算:
i
,并选择一个 严格小于 nums[i]
的质数 p
,从 nums[i]
中减去 p
。如果你能通过上述运算使得 nums
成为严格递增数组,则返回 true
;否则返回 false
。
严格递增数组 中的每个元素都严格大于其前面的元素。
示例 1:
输入:nums = [4,9,6,10]
输出:true
解释:
在第一次运算中:选择 i = 0 和 p = 3 ,然后从 nums[0] 减去 3 ,nums 变为 [1,9,6,10] 。
在第二次运算中:选择 i = 1 和 p = 7 ,然后从 nums[1] 减去 7 ,nums 变为 [1,2,6,10] 。
第二次运算后,nums 按严格递增顺序排序,因此答案为 true 。
示例 2:
输入:nums = [6,8,11,12]
输出:true
解释:nums 从一开始就按严格递增顺序排序,因此不需要执行任何运算。
示例 3:
输入:nums = [5,8,3]
输出:false
解释:可以证明,执行运算无法使 nums 按严格递增顺序排序,因此答案是 false 。
提示:
1 <= nums.length <= 1000
1 <= nums[i] <= 1000
nums.length == n
class Solution {
public boolean primeSubOperation(int[] nums) {
int n = 2000;
// 欧拉筛
//判断是否是质数,1-质数 0-合数
int[] isPrime = new int[n];
//存放质数
int[] primes = new int[n];
int k = 0;//存放质数数组的索引下标
Arrays.fill(isPrime, 1);
isPrime[1] = 0;
for (int i = 2; i < n; i++) {
if (isPrime[i] == 1) {
primes[k++] = i;
}
//枚举已经筛出来的素数prime[j](j=1~cnt)
for (int j = 0; primes[j] * i < n; j++) {
//筛掉i的素数倍,即i的prime[j]倍
isPrime[primes[j] * i] = 0;//每个质数都和i相乘得到合数
//如果i整除prime[j],退出循环,保证线性时间复杂度
if (i % primes[j] == 0) {//primes[j]是i的一个质因数
break;
}
}
}
int pre = 0; // 当前枚举i位上前一个元素值(每次都让其最小)
for(int i = 0; i < nums.length; i++){
int cur = nums[i];
// 不满足直接false,因为pre已经是最小了,cur还比pre小
if(cur <= pre) return false;
// 让当前数达到能达到的最小值
while(true){
if(isPrime[cur] == 1){
if(pre < nums[i] - cur){
pre = nums[i] - cur;
break;
}
}
cur--;
}
}
return true;
}
难度中等0
给你一个正整数数组 nums
。
同时给你一个长度为 m
的整数数组 queries
。第 i
个查询中,你需要将 nums
中所有元素变成 queries[i]
。你可以执行以下操作 任意 次:
1
。请你返回一个长度为 m
的数组 answer
,其中 answer[i]
是将 nums
中所有元素变成 queries[i]
的 最少 操作次数。
注意,每次查询后,数组变回最开始的值。
示例 1:
输入:nums = [3,1,6,8], queries = [1,5]
输出:[14,10]
解释:第一个查询,我们可以执行以下操作:
- 将 nums[0] 减小 2 次,nums = [1,1,6,8] 。
- 将 nums[2] 减小 5 次,nums = [1,1,1,8] 。
- 将 nums[3] 减小 7 次,nums = [1,1,1,1] 。
第一个查询的总操作次数为 2 + 5 + 7 = 14 。
第二个查询,我们可以执行以下操作:
- 将 nums[0] 增大 2 次,nums = [5,1,6,8] 。
- 将 nums[1] 增大 4 次,nums = [5,5,6,8] 。
- 将 nums[2] 减小 1 次,nums = [5,5,5,8] 。
- 将 nums[3] 减小 3 次,nums = [5,5,5,5] 。
第二个查询的总操作次数为 2 + 4 + 1 + 3 = 10 。
示例 2:
输入:nums = [2,9,6,3], queries = [10]
输出:[20]
解释:我们可以将数组中所有元素都增大到 10 ,总操作次数为 8 + 1 + 4 + 7 = 20 。
提示:
n == nums.length
m == queries.length
1 <= n, m <= 105
1 <= nums[i], queries[i] <= 109
思考过程:https://leetcode.cn/problems/minimum-operations-to-make-all-array-elements-equal/solution/pai-xu-qian-zhui-he-er-fen-cha-zhao-by-s-t2mn/
每次+1或-1,那么其实就是每个数对当前queries[i]的差值,一个一个算肯定不行,举个例子对于3来说,1和2于它的差值为2*3-(1+2),那么其实就是n个3相乘减去n个数的和,那么我们可以利用前缀和加速
数组中有大于等于queries[i]和小于的,那么分成两段计算,我们就要找一个分界点,我们对数组排序,通过二分查找找到第一个>=queries[i]的位置,然后对这个位置分两段来计算
特殊情况,所有数都小于queries[i],那么我们就不用分类了,直接算就行
class Solution {
public List<Long> minOperations(int[] nums, int[] queries) {
Arrays.sort(nums);
List<Long> res = new ArrayList<>();
int n = nums.length;
long[] sum = new long[n+1];
for(int i = 0; i < n; i++){
sum[i+1] = sum[i] + nums[i];
}
for(int q : queries){
// 找到第一个比q大的元素下标
int left = 0, right = n;
while(left < right) {
int mid = (left + right) >> 1;
if(nums[mid] <= q) left = mid + 1;
else right = mid;
}
// 在left=right左边的都比q小,右边的都比q大
// 用目标和-前缀和求左边的操作数 用前缀和-目标和求右边的操作数
long cur = 0l;
cur += (long)q * (long)left - sum[left]; //左边: 目标和 - 前缀和
cur += sum[n] - sum[left] - (long)q * (long)(n-left); //右边:前缀和 - 目标和
res.add((long)cur);
}
return res;
}
}
Go
func minOperations(nums []int, queries []int) []int64 {
sort.Ints(nums);
n := len(nums)
s := make([]int, n+1)
for i := 0; i < n; i++ {
s[i+1] = s[i] + nums[i]
}
res := make([]int64, len(queries))
for i, q := range queries {
// 在nums数组中找到第一个小于q的元素位置
// go有api可以方便查找
//right := sort.SearchInts(nums, q)
left, right := 0, n
for left < right {
mid := (left + right) >> 1;
if nums[mid] < q {
left = mid + 1;
} else {
right = mid;
}
}
l := right * q - s[right];
r := s[n] - s[right] - (n - right) * q
res[i] += int64(l + r)
}
return res
}
难度困难5
给你一个 n
个节点的无向无根树,节点编号从 0
到 n - 1
。给你整数 n
和一个长度为 n - 1
的二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间有一条边。再给你一个长度为 n
的数组 coins
,其中 coins[i]
可能为 0
也可能为 1
,1
表示节点 i
处有一个金币。
一开始,你需要选择树中任意一个节点出发。你可以执行下述操作任意次:
2
以内的所有金币,或者你需要收集树中所有的金币,并且回到出发节点,请你返回最少经过的边数。
如果你多次经过一条边,每一次经过都会给答案加一。
示例 1:
输入:coins = [1,0,0,0,0,1], edges = [[0,1],[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:从节点 2 出发,收集节点 0 处的金币,移动到节点 3 ,收集节点 5 处的金币,然后移动回节点 2 。
示例 2:
输入:coins = [0,0,0,1,1,0,0,1], edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[5,6],[5,7]]
输出:2
解释:从节点 0 出发,收集节点 4 和 3 处的金币,移动到节点 2 处,收集节点 7 处的金币,移动回节点 0 。
提示:
n == coins.length
1 <= n <= 3 * 104
0 <= coins[i] <= 1
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges
表示一棵合法的树。题解:https://leetcode.cn/problems/collect-coins-in-a-tree/solution/ceng-ceng-bo-chi-mei-yong-de-jie-dian-le-w10u/
0x3f:https://leetcode.cn/problems/collect-coins-in-a-tree/solution/tuo-bu-pai-xu-ji-lu-ru-dui-shi-jian-pyth-6uli/
class Solution {
// 性质:如果所有在叶子上的金币都收集到了,那么顺路可以把不在叶子上的金币页收集到
// 从没有金币的叶子出发,跑拓扑排序。
// 再次拓扑排序,去掉两轮叶子
public int collectTheCoins(int[] coins, int[][] edges) {
int n = coins.length;
List<Integer> g[] = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
int[] indegree = new int[n]; // 入度
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);//建图
++indegree[x];
++indegree[y];//
}
// 用拓扑排序剪枝:去掉没有金币的子树
Deque<Integer> dq = new ArrayDeque<>();
for(int i = 0; i < n; i++){
if(indegree[i] == 1 && coins[i] == 0){ // 无金币叶子
dq.add(i);
}
}
while(!dq.isEmpty()){
int x = dq.peek();
dq.pop();
for(int y : g[x]){
if(--indegree[y] == 1 && coins[y] == 0) { // 无金币叶子的链路都不要
dq.add(y);
}
}
}
// 再跑两次拓扑排序:删除树中所有叶子节点,及其相邻边(删两次)
// 所有的含金币的叶子节点,其实也是不需要被访问的,只需要距离 < 2 就可以了
for(int i = 0; i < n; i++){
if(indegree[i] == 1 && coins[i] == 1){ // 有金币的叶子
dq.add(i);
}
}
if(dq.size() <= 1) return 0; // 至多一个有金币的叶子,直接收集
// 我们得到了一个 ”核心树“,其中每个节点都必须要访问。
// 从树的任意节点出发,访问所有节点然后返回,每条边恰好访问两次。因此答案就是核心树上的边数
int[] time = new int[n];
while(!dq.isEmpty()){
int x = dq.peek();
dq.pop();
for(int y : g[x]){
if(--indegree[y] == 1) { // 记录入队时间
time[y] = time[x] + 1;
dq.add(y);
}
}
}
// 统计答案 答案就是剩下的树中的边数
int ans = 0;
for(int[] e : edges){
if(time[e[0]] >= 2 && time[e[1]] >= 2){
ans += 2;
}
}
return ans;
}
}