leetcode 第360场周赛

总结

好久没参加leetcode周赛了,比赛时间都从两小时变成了一个半小时。这次周赛由两道签到题和两道中等难度题组成,严格来说最后一道的难度也可以视为hard,但是只要想到正确的思路,编码还是比较容易的。

比赛链接:leetcode 第 360 场周赛

题目列表

1.距离原点最远的点

题目描述

给你一个长度为 n 的字符串 moves ,该字符串仅由字符 'L''R''_' 组成。字符串表示你在一条原点为 0 的数轴上的若干次移动。

你的初始位置就在原点(0),第 i 次移动过程中,你可以根据对应字符选择移动方向:

  • 如果 moves[i] = 'L'moves[i] = '_' ,可以选择向左移动一个单位距离
  • 如果 moves[i] = 'R'moves[i] = '_' ,可以选择向右移动一个单位距离

移动 n 次之后,请你找出可以到达的距离原点 最远 的点,并返回 从原点到这一点的距离

示例 1:

输入:moves = "L_RL__R"
输出:3
解释:可以到达的距离原点 0 最远的点是 -3 ,移动的序列为 "LLRLLLR" 。

示例 2:

输入:moves = "_R__LL_"
输出:5
解释:可以到达的距离原点 0 最远的点是 -5 ,移动的序列为 "LRLLLLL" 。

示例 3:

输入:moves = "_______"
输出:7
解释:可以到达的距离原点 0 最远的点是 7 ,移动的序列为 "RRRRRRR" 。

提示:

  • 1 <= moves.length == n <= 50
  • moves 仅由字符 'L''R''_' 组成

分析

本题主要是理解题意,moves是'_'的时候可以选择左移也可以选择右移,要求最终到达的位置离原点最远。

移动的顺序无所谓,因此经过若干次'L'和'R'的移动后,位置P是唯一的,此时只剩下若干个''的操作符,如果P是在原点左边,''就继续左移,否则右移。具体实现时候只需要统计下各个操作字符的个数,然后返回 a b s ( L − R ) + c n t ( _ ) abs(L-R) + cnt(\) abs(L−R)+cnt()即可。

代码

cpp 复制代码
class Solution {
public:
    int furthestDistanceFromOrigin(string moves) {
        int l = 0, r = 0, t = 0;
        for(int i = 0;i < moves.size();i++) {
            if (moves[i] == 'L') l++;
            else if(moves[i] == 'R') r++;
            else t++;
        }
        return max(l, r)  - min(l, r) + t;
    }
};

2.找出美丽数组的最小和

题目描述

给你两个正整数:ntarget

如果数组 nums 满足下述条件,则称其为 美丽数组

  • nums.length == n.
  • nums 由两两互不相同的正整数组成。
  • 在范围 [0, n-1] 内,不存在 两个 不同 下标 ij ,使得 nums[i] + nums[j] == target

返回符合条件的美丽数组所可能具备的 最小 和。

示例 1:

输入:n = 2, target = 3
输出:4
解释:nums = [1,3] 是美丽数组。
- nums 的长度为 n = 2 。
- nums 由两两互不相同的正整数组成。
- 不存在两个不同下标 i 和 j ,使得 nums[i] + nums[j] == 3 。
可以证明 4 是符合条件的美丽数组所可能具备的最小和。

示例 2:

输入:n = 3, target = 3
输出:8
解释:
nums = [1,3,4] 是美丽数组。 
- nums 的长度为 n = 3 。 
- nums 由两两互不相同的正整数组成。 
- 不存在两个不同下标 i 和 j ,使得 nums[i] + nums[j] == 3 。
可以证明 8 是符合条件的美丽数组所可能具备的最小和。

示例 3:

输入:n = 1, target = 1
输出:1
解释:nums = [1] 是美丽数组。

提示:

  • 1 <= n <= 10^5
  • 1 <= target <= 10^5

分析

简单的模拟题,从1开始枚举数组里的正整数,添加i前确认下数组里不存在target - i的数字,添加后就使用哈希表记录下已添加的数字,直至枚举到n个数,求和即可。

代码

cpp 复制代码
class Solution {
public:
    unordered_map<int,int> m;
    long long minimumPossibleSum(int n, int target) {
        long long res = 0;
        int cnt = 0;
        int num = 1;
        while(cnt < n) {
            if (!m.count(target - num)) {
                cnt++;
                m[num] = 1;
                res += num;
            }
            num++;
        }
        return res;
    }
};

3.使子序列的和等于目标的最少操作次数

题目描述

给你一个下标从 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] <= 2^30
  • nums 只包含非负整数,且均为 2 的幂。
  • 1 <= target < 2^31

分析

首先,数组里面的元素都是2的整数次幂,只要数组的和不小于target,那么经过若干次拆分一定可以使得子序列的和等于target。

不难想到将target使用二进制表示,拆分成若干个2的整数次幂数字的和,然后在数组里面找到这些数字,如果不存在,要么拆分,要么使用更小的数累加。

nums = [1,32,1,2], target = 12

第二个示例比较适合用来分析,target为12=4 + 8,数组里找不到4,所以使用两个1和一个2拼到一起了;然后32拆分两次得到8。什么时候使用拼接,什么时候使用拆分?想清楚这个问题本题就可以求解了。

求解思路如下:

  • 将target的二进制表示存入vector s,将数组里面的元素二进制表示也存入一个vector v。
  • 自低位到高位遍历target的二进制vector,如果该位置是1,就在v中查找有没有对应的元素,有就将该元素数量减一,将多余的元素除以2累加到更高位的元素数量,没有就继续往高位查找,找到高位元素后一步步分解,分解到需要的元素重复前面的步骤;如果s的该位置是0,也需要将v中对应元素除以2累加到高位。

以上面例子为例,12=(1100)~2~,最低位是0,而nums里面有2个1,将这两个1累加起来增加2的个数,得到[32,2,2];第二位还是0,将两个2累加起来得到[32,4],第三位是1,并且数组里面出现了累加的4;第四位是1,数组里面没有找到8,向高位找到了32,拆分两次得到了8,最终拆分的次数是2。

因此,本题的关键就是想到对于没用完的元素,累加到高位继续用,想到了这点直接模拟就可以解决了。

代码

python 复制代码
class Solution {
public:
    int index(int n) {
        if (n == 1) return 0;
        return index(n / 2) + 1;
    }
    int minOperations(vector<int>& nums, int target) {
        vector<int> v(32, 0);
        vector<int> s;
        int t = target;
        while(t) {
            s.push_back(t & 1);
            t >>= 1;
        }
        int n = s.size();
        long long sum = 0;
        for(auto x : nums) {
            v[index(x)]++;
            sum += x;
        }
        if (sum < target) return -1;
        int res = 0;
        for (int i = 0; i < n;i++) {
            if (s[i]) {
                if (v[i]) {
                    v[i]--;
                    v[i+1] += v[i] / 2;
                    continue;
                }
                int j = i + 1;
                while(!v[j]) j++;
                while(j != i) {
                    v[j]--;
                    v[--j] += 2;
                    res++;
                }
            } else {
                v[i+1] += v[i] / 2;
            }
        }
        return res;
    }
};

4.在传球游戏中最大化函数值

题目描述

给你一个长度为 n 下标从 0 开始的整数数组 receiver 和一个整数 k

总共有 n 名玩家,玩家 编号 互不相同,且为 [0, n - 1] 中的整数。这些玩家玩一个传球游戏,receiver[i] 表示编号为 i 的玩家会传球给编号为 receiver[i] 的玩家。玩家可以传球给自己,也就是说 receiver[i] 可能等于 i

你需要从 n 名玩家中选择一名玩家作为游戏开始时唯一手中有球的玩家,球会被传 恰好 k 次。

如果选择编号为 x 的玩家作为开始玩家,定义函数 f(x) 表示从编号为 x 的玩家开始,k 次传球内所有接触过球玩家的编号之 ,如果有玩家多次触球,则 累加多次 。换句话说, f(x) = x + receiver[x] + receiver[receiver[x]] + ... + receiver(k)[x]

你的任务时选择开始玩家 x ,目的是 最大化 f(x)

请你返回函数的 最大值

注意: receiver 可能含有重复元素。

示例 1:

传递次数 传球者编号 接球者编号 x + 所有接球者编号
2
1 2 1 3
2 1 0 3
3 0 2 5
4 2 1 6
输入:receiver = [2,0,1], k = 4
输出:6
解释:上表展示了从编号为 x = 2 开始的游戏过程。
从表中可知,f(2) 等于 6 。
6 是能得到最大的函数值。
所以输出为 6 。

示例 2:

传递次数 传球者编号 接球者编号 x + 所有接球者编号
4
1 4 3 7
2 3 2 9
3 2 1 10
输入:receiver = [1,1,1,2,3], k = 3
输出:10
解释:上表展示了从编号为 x = 4 开始的游戏过程。
从表中可知,f(4) 等于 10 。
10 是能得到最大的函数值。
所以输出为 10 。

提示:

  • 1 <= receiver.length == n <= 10^5
  • 0 <= receiver[i] <= n - 1
  • 1 <= k <= 10^10

分析

本题常规解法,找环的起点比较繁琐,而且即使找到了环,如果环的长度是10w级别的,遍历下n个起点,时间也会超限。比较好的解法是使用DP+倍增。一共要传k轮,可以对k进行二进制拆分,比如k = 13,可以拆除1 + 4 + 8,也就是先传1轮,再传4轮,最后传8轮。

状态表示: f [ i ] [ j ] f[i][j] f[i][j]表示从 i i i开始,传 2 j 2^j 2j轮经过的编号之和,注意这里的编号不包括 i i i本身,因为一旦包含了 i i i,第一次传到了 t t t,已经统计了 t t t的编号,第二次从 t t t开始传,就重复统计了,统计编号时不包含起点,最后统计结果时再加上起点的编号即可。

另外,除了需要记录传 2 j 2^j 2j后的编号之和,还需要记录位置,这样才能确定下一轮传输的起点,即 p o s [ i ] [ j ] pos[i][j] pos[i][j]表示从 i i i开始,传 2 j 2^j 2j轮到达的编号。

状态转移方程:
f [ i ] [ j ] = f [ i ] [ j − 1 ] + f [ p o s [ i ] [ j − 1 ] ] [ j − 1 ] f[i][j] = f[i][j-1] + f[pos[i][j-1]][j-1] f[i][j]=f[i][j−1]+f[pos[i][j−1]][j−1]

p o s [ i ] [ j ] = p o s [ p o s [ i ] [ j − 1 ] ] [ j − 1 ] pos[i][j] = pos[pos[i][j-1]][j-1] pos[i][j]=pos[pos[i][j−1]][j−1]

也就是从 i i i出发走 2 j 2^j 2j步,只需要从 i i i出发先走 2 j − 1 2^{j-1} 2j−1步到达 p o s [ i ] [ j − 1 ] pos[i][j-1] pos[i][j−1]的位置,再从该位置出发再走 2 j − 1 2^{j-1} 2j−1步。

状态边界: f [ i ] [ 0 ] = r e c e i v e r [ i ] f[i][0]=receiver[i] f[i][0]=receiver[i]和 p o s [ i ] [ 0 ] = r e c e i v e r [ i ] pos[i][0]=receiver[i] pos[i][0]=receiver[i]。

另外,还需要记录下走 k k k轮,每次走到的中转的位置,这样才能在此基础上找到下次的起点。

由于状态转移每次只使用了上一层的状态,所以可以使用滚动数组实现,节省内存空间。

具体实现见代码:

代码

cpp 复制代码
class Solution {
public:
    static const int N = 100005;
    long long f[N][2];
    int pos[N][2], pre[N];
    long long ans[N];
    long long getMaxFunctionValue(vector<int>& r, long long k) {
        int n = r.size();
        for (int i = 0; i < n;i++) {
            f[i][0] = r[i];
            pos[i][0] = r[i];
            pre[i] = i;
        }
        if (k & 1) {
            for(int i = 0; i < n;i++) {
                ans[i] = f[i][0];
                pre[i] = pos[i][0];
            }
        }
        for (int i = 1; i < 35;i++) {
            int t1 = i & 1, t2 = 1 - t1;
            for (int j = 0; j < n;j++) {
                f[j][t1] = f[j][t2] + f[pos[j][t2]][t2];
                pos[j][t1] = pos[pos[j][t2]][t2];
            }
            if(k >> i & 1) {
                for(int j = 0; j < n;j++) {
                    ans[j] += f[pre[j]][t1];
                    pre[j] = pos[pre[j]][t1];
                }
            }
        }
        long long res = 0;
        for(int i = 0; i < n;i++) res = max(res, i + ans[i]);
        return res;
    }
};
相关推荐
dengqingrui1231 个月前
【树形DP】AT_dp_p Independent Set 题解
c++·学习·算法·深度优先·图论·dp
疯狂的小强呀1 个月前
超详细讲解:DP和DDP的区别以及使用方法
算法·dp·ddp
Jcqsunny1 个月前
[dp] 小信走迷宫
算法·前缀和·动态规划·dp
源代码•宸2 个月前
Leetcode—322. 零钱兑换【中等】(memset(dp,0x3f, sizeof(dp))
c++·算法·leetcode·职场和发展·dp
Qres8212 个月前
[SCOI2014] 方伯伯的玉米田(dp+树状数组维护行列)
数据结构·dp·ds
源代码•宸2 个月前
Leetcode—72. 编辑距离【中等】
c++·算法·leetcode·dp
WJX5742 个月前
最长公共子序列
dp
sweetheart7-72 个月前
LeetCode152. 乘积最大子数组(2024秋季每日一题 2)
数据结构·算法·动态规划·dp·每日一题
源代码•宸3 个月前
Leetcode—1143. 最长公共子序列【中等】
c++·经验分享·算法·leetcode·dp
rgw20103 个月前
P1973 [NOI2011] NOI 嘉年华
指针·dp·noi·线段树优化 dp