庆祝我的第一次AK,哟哟哟,以为可以挤进前100,但是大佬还是多呀。主要是第二题的map处理之后,不知道要怎么排序,所以卡了挺久的,可怜。
周赛排名:145/ 1648
第一题:模拟。
第二题:前缀 + 异或。
第三题:BFS + map + 排序。
第四题:动态规划 DP。
详细题解如下。
1. 解码字母到整数映射(Decrypt String From Alphabet to Integer Mapping)
2. 子数组异或查询(Xor Queries of A Subarray)
3.获取你好友已观看的视频(Get Watched Videos by Your Friends)
AC代码(方法一 BFS + map + 对map枚举排序 C++)
AC代码(方法二 BFS+map+重写sort排序 C++)
4.让字符串成为回文串的最少插入次数(Minimum Insertion Steps to Make A String Palindrome)
LeetCode第170场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-170/
1. 解码字母到整数映射(Decrypt String From Alphabet to Integer Mapping)
题目链接
https://leetcode-cn.com/problems/decrypt-string-from-alphabet-to-integer-mapping/
题意
给你一个字符串 s,它由数字('0' - '9')和 '#' 组成。我们希望按下述规则将 s 映射为一些小写英文字符:
- 字符('a' - 'i')分别用('1' - '9')表示。
- 字符('j' - 'z')分别用('10#' - '26#')表示。
返回映射之后形成的新字符串。
题目数据保证映射始终唯一。
示例 1:
输入:s = "10#11#12" 输出:"jkab" 解释:"j" -> "10#" , "k" -> "11#" , "a" -> "1" , "b" -> "2".
示例 2:
输入:s = "1326#" 输出:"acz"
示例 3:
输入:s = "25#" 输出:"y"
示例 4:
输入:s = "12345678910#11#12#13#14#15#16#17#18#19#20#21#22#23#24#25#26#" 输出:"abcdefghijklmnopqrstuvwxyz"
提示:
1 <= s.length <= 1000
s[i]
只包含数字('0'
-'9'
)和'#'
字符。s
是映射始终存在的有效字符串。
解题思路
理解题目的意思,其实就是将 a - z 分别映射成了 1 - 26,但是由于有一位数和两位数,为了一一映射,将两位数的后面加上 #,这样子就互不相同。
现在是给出,映射之后字符串,让我们得出原来的包含 a - z 的字符串。
如果我们从前往后扫过去,判断会比较麻烦,比如我们遇到 1,那么要看 1 的后两位是不是要为 #,如果是的话,那就是 1 和 后面的那个数,变成两位数,再映射回 字母。
但是如果我们从后往前扫,只要一遇到 #,那么我们就知道 # 的前两位数,就是一起映射的,如果直接遇到的是 数字,那就就是一位数的映射。
(从前往后 也是可以做的,但是 从后往前 更加方便判断)
AC代码(C++)
class Solution {
public:
string freqAlphabets(string s) {
string ans = "";
int temp = 0;
for(int i = s.size()-1;i >= 0; --i)
{
// 如果遇到 #,说明是两位数
if(s[i] == '#')
{
temp = (s[i-2] - '0')*10 + (s[i-1] - '0');
i -= 2;
}
else // 否则直接是 一位数
{
temp = s[i] - '0';
}
// 得到了 1- 26,对应映射回 a-z,注意,因为是从后往前,所以新加进去的是在左边。
ans = (char)((temp - 1) + 'a') + ans;
}
return ans;
}
};
2. 子数组异或查询(Xor Queries of A Subarray)
题目链接
https://leetcode-cn.com/problems/xor-queries-of-a-subarray/
题意
有一个正整数数组 arr,现给你一个对应的查询数组 queries,其中 queries[i] = [Li, Ri]。
对于每个查询 i,请你计算从 Li 到 Ri 的 XOR 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri])作为本次查询的结果。
并返回一个包含给定查询 queries 所有结果的数组。
示例 1:
输入:arr = [1,3,4,8], queries = [[0,1],[1,2],[0,3],[3,3]] 输出:[2,7,14,8] 解释: 数组中元素的二进制表示形式是: 1 = 0001 3 = 0011 4 = 0100 8 = 1000 查询的 XOR 值为: [0,1] = 1 xor 3 = 2 [1,2] = 3 xor 4 = 7 [0,3] = 1 xor 3 xor 4 xor 8 = 14 [3,3] = 8
示例 2:
输入:arr = [4,8,2,10], queries = [[2,3],[1,3],[0,0],[0,3]] 输出:[8,0,4,4]
提示:
- 1 <= arr.length <= 3 * 10^4
- 1 <= arr[i] <= 10^9
- 1 <= queries.length <= 3 * 10^4
- queries[i].length == 2
- 0 <= queries[i][0] <= queries[i][1] < arr.length
解题思路
拿到题目,第一想法,暴力,那么时间复杂度是 O(Q * A),其中Q是queries的长度,A是arr的长度。这样子的话,根据数据范围,会超时。
因此需要考虑到其他方法,题目问的是,异或,那么要想到异或的性质
- 相同的数,异或 = 0
- 任何数 和 0 异或还是其本身
如果我们能在每一个queries[i] = [Li, Ri] 中,能够快速的得出答案,即 O(1)的话,那么就不会超时,也就是空间换时间,提前处理。
- 我们可以用一个前缀和 记录前 n 个数的异或值
- 0,1,2....L-1 个数的异或值为 A
- 0,1,2,...R 个数的异或值为 B
- 那么问 [L, R] 区间中的异或为 A^B
因为我们的前 R个的异或值中,有 前 L - 1 数的异或值,那么我们将这两个值再进行异或,那就是,相当于把 前 R个中去掉了前 L - 1 个,那么剩下的异或值,就是 [L, R] 区间中数的异或值。
这就利用了,相同数异或 = 0的性质。
AC代码(C++)
class Solution {
public:
vector<int> xorQueries(vector<int>& arr, vector<vector<int>>& queries) {
int n = arr.size();
vector<int> ans;
// 因为对于 L-1 可能会小于0,所以我们对前缀和数组,增加一位,使得不会越界
vector<int> yihuo(n + 1, 0);
int cnt = 0;
for(int i = 0;i < n;i++)
{
cnt ^= arr[i];
yihuo[i + 1] = cnt;
}
for(auto q : queries)
{
// 因为增加了一位,所以 R 变成 R+1, L-1变成了L
int temp = yihuo[q[1]+1] ^ yihuo[q[0]];
ans.push_back(temp);
}
return ans;
}
};
3.获取你好友已观看的视频(Get Watched Videos by Your Friends)
题目链接
https://leetcode-cn.com/problems/get-watched-videos-by-your-friends/
题意
有 n 个人,每个人都有一个 0 到 n-1 的唯一 id 。
给你数组 watchedVideos 和 friends ,其中 watchedVideos[i] 和 friends[i] 分别表示 id = i 的人观看过的视频列表和他的好友列表。
Level 1 的视频包含所有你好友观看过的视频,level 2 的视频包含所有你好友的好友观看过的视频,以此类推。一般的,Level 为 k 的视频包含所有从你出发,最短距离为 k 的好友观看过的视频。
给定你的 id 和一个 level 值,请你找出所有指定 level 的视频,并将它们按观看频率升序返回。如果有频率相同的视频,请将它们按名字字典序从小到大排列。
示例 1:
输入:watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 1 输出:["B","C"] 解释: 你的 id 为 0 ,你的朋友包括: id 为 1 -> watchedVideos = ["C"] id 为 2 -> watchedVideos = ["B","C"] 你朋友观看过视频的频率为: B -> 1 C -> 2
示例 2:
输入:watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 2 输出:["D"] 解释: 你的 id 为 0 ,你朋友的朋友只有一个人,他的 id 为 3 。
提示:
n == watchedVideos.length == friends.length
2 <= n <= 100
1 <= watchedVideos[i].length <= 100
1 <= watchedVideos[i][j].length <= 8
0 <= friends[i].length < n
0 <= friends[i][j] < n
0 <= id < n
1 <= level < n
如果 friends[i] 包含 j ,那么 friends[j] 包含 i
解题分析
根据题目,我们要先找到处于要求的 level 上的所有人,然后再去统计这些人的观看视频(包括看了啥视频,这些视频分别看了多少次),最后根据要求进行排序输出看了啥视频(先按观看频率升序排序,如果频率相同,就按视频名的字典序排序)。
因此这道题总共就是三步走。
1、其中第一个,其实就是构造一个树,然后以 id 为根节点,找到深度为 level 上的所有节点(即对应的人)。
所以第一想到就是利用BFS,找出这些人,存放在一个数组中。所以第一步就是简单的BFS
2、找到了所有的人之后,我们就统计对应这些人观看的视频,要统计看了哪些视频和每个视频对应看了几次。
所以我们想到用 map<string, int>,key表示视频名,value表示这个视频看了几次
3、按照要求进行排序
本来要对map进行排序的,但是map不能排序。而且用的是map,本身是红黑树,已经是给了 key 自动排序了。但是我们不仅仅要对视频名的字典序进行排序,还要按照观看频率,即map的value排序。
1)所以我自己做的时候,想的是暴力排序,hhh,就是我们之前统计了视频不同的数量,然后枚举这些数量,最后对每一次都遍历所有map中的视频和对应的次数,(因为是先按观看次数升序),所以找到最小的次数,同时,由于相同观看次数的时候,字典序小的肯定是在前面,我们统计最小数的时候,记录下,第一个出现最小数的视频名。这样子这个视频名就是排序后的前一个,存放在答案中 vector<string> ans。
接着我们把对应这个视频出现次数设为最大值,表示用过了。接着下一次遍历。
虽然这样子时间复杂度变大,但是不会超时。
2)后面看了大佬们的做法,也同样先用map统计了每种视频对应的观看次数。并且先把所有出现的视频名都放到 vector<string> ans 中。
然后重写排序 sort 中的cmp,cmp的规则是,先按 对应视频名统计出现的次数 mp[视频名],来排序。如果两个次数都相同,就对字符串进行字典序排序。
这样子时间复杂度会降低。
AC代码(方法一 BFS + map + 对map枚举排序 C++)
const int MAXN = 100 + 10;
#define INF 0x3f3f3f3f
class Solution {
public:
// 用于 BFS的,记录点 x和对应的深度d
struct Node{
int x;
int d;
Node(int x_, int d_): x(x_), d(d_) {}
};
map<string, int> mp;
vector<int> f;
int vis[MAXN];
// BFS
void bfs(vector<vector<int>>& friends, int id, int level)
{
memset(vis, 0, sizeof(vis));
queue<Node> q;
q.push(Node(id, 0));
vis[id] = 1;
while(!q.empty())
{
Node cur = q.front();
q.pop();
if(cur.d > level) break; //如果出现深度 > 要求的,那么说明对应的level深度已经找完了,就可以提前结束BFS了
if(cur.d == level) f.push_back(cur.x); // 等于深度,就存放起来
for(int i = 0;i < friends[cur.x].size(); ++i)
{
if(vis[friends[cur.x][i]] == 0)
{
q.push(Node(friends[cur.x][i], cur.d + 1));
vis[friends[cur.x][i]] = 1;
}
}
}
return;
}
vector<string> watchedVideosByFriends(vector<vector<string>>& watchedVideos, vector<vector<int>>& friends, int id, int level) {
vector<string> ans;
// 1、先BFS
bfs(friends, id, level);
// 2、用map统计出现的次数
int numVide = 1;
for(int i = 0;i < f.size();i++)
{
for(int j = 0;j < watchedVideos[f[i]].size(); ++j)
{
if(mp[watchedVideos[f[i]][j]] == 0)
{
mp[watchedVideos[f[i]][j]] = 1;
numVide++;
}
else
mp[watchedVideos[f[i]][j]]++;
}
}
// 3、暴力对map排序
while(--numVide)
{
int minVal = INF; // 找的是最小值
string res;
for(map<string,int>::iterator it=mp.begin();it!=mp.end();++it)
{
if((it->second) < minVal)
{
minVal = it->second; // 记录出现的最小值
}
}
for(map<string,int>::iterator it=mp.begin();it!=mp.end();++it)
{
if((it->second) == minVal) // 从前往后,第一个对应最小值,同时也是字典序最小
{
res = it->first;
it->second = INF; // 这个已经输出了,所以变为最大值,下次不会搜索到
break;
}
}
ans.push_back(res);
}
return ans;
}
};
AC代码(方法二 BFS+map+重写sort排序 C++)
const int MAXN = 100 + 10;
map<string, int> mp;
// 重写cmp
inline bool cmp(const string &a, const string &b)
{
if(mp[a] != mp[b]) return mp[a] < mp[b]; //先按频率排序
//频率相同,就按字符串排序
return a < b;
}
class Solution {
public:
struct Node{
int x;
int d;
Node(int x_, int d_): x(x_), d(d_) {}
};
vector<int> f;
int vis[MAXN];
void bfs(vector<vector<int>>& friends, int id, int level)
{
memset(vis, 0, sizeof(vis));
queue<Node> q;
q.push(Node(id, 0));
vis[id] = 1;
while(!q.empty())
{
Node cur = q.front();
q.pop();
if(cur.d > level) break;
if(cur.d == level) f.push_back(cur.x);
for(int i = 0;i < friends[cur.x].size(); ++i)
{
if(vis[friends[cur.x][i]] == 0)
{
q.push(Node(friends[cur.x][i], cur.d + 1));
vis[friends[cur.x][i]] = 1;
}
}
}
return;
}
vector<string> watchedVideosByFriends(vector<vector<string>>& watchedVideos, vector<vector<int>>& friends, int id, int level) {
vector<string> ans;
// 1、BFS
bfs(friends, id, level);
// 2、用map统计次数
mp.clear(); // 需要把 mp 清除内存,安全
for(int i = 0;i < f.size();i++)
{
for(int j = 0;j < watchedVideos[f[i]].size(); ++j)
{
if(mp[watchedVideos[f[i]][j]] == 0)
{
ans.push_back(watchedVideos[f[i]][j]); //出现新的视频,放入ans
mp[watchedVideos[f[i]][j]] = 1;
}
else
mp[watchedVideos[f[i]][j]]++;
}
}
sort(ans.begin(), ans.end(), cmp); // 按照要求排序,重写cmp
return ans;
}
};
4.让字符串成为回文串的最少插入次数(Minimum Insertion Steps to Make A String Palindrome)
题目链接
https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/
题意
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
示例 1:
输入:s = "zzazz" 输出:0 解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。
示例 2:
输入:s = "mbadm" 输出:2 解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
示例 3:
输入:s = "leetcode" 输出:5 解释:插入 5 个字符后字符串变为 "leetcodocteel" 。
示例 4:
输入:s = "g" 输出:0
示例 5:
输入:s = "no" 输出:1
提示:
1 <= s.length <= 500
s
中所有字符都是小写字母。
解题分析
最小操作次数,所以考虑一下,能不能使用动态规划DP
1)设变量,因为字符串是不是回文串,那就是从某一位置到另一位置,因此要用二维的DP。所以设 dp[ i ][ j ] 表示,从 i 到 j 字符串要是回文串的最小操作次数。
2)状态转移当我们判断,从 i 到 j 的时候,如果 s[ i ] == s[ j ],那就说明这两个字符相同,不会增加操作次数,那么 dp[ i ][ j ] = dp[ i+1 ][ j-1 ],也就是由内部的操作次数决定。如果 s[ i ] != s[ j ],说明这两个字符相同,那么需要增加(其实也可以看成是删除)一个的时候。那么我们可以选择在左边增加,或者右边增加,那么因为是最小操作,所以取最小值 min(dp[ i+1 ][ j], dp[ i][ j-1 ] ) + 1 ,在最小值的基础上,因为不同,所以要操作一次,因为操作数是 min + 1。
3)我们判断字符串,肯定是从 (0, n-1),往内部看的,所以用到递归,那么就会有重复的值,因此用记忆化搜索,即如果已经计算过的值,我们就直接用。因此我们设 dp 的初始值为 -1,只要需要不是 -1(因为处理过的,操作数至少会是0),那么就说明计算过的值,直接返回。
同时边界条件是,当 (i, j) 表示的字符串长度为 1或者0的时候,相当于本身就是回文串,那么操作数为0。
AC代码(Java)
import java.util.Arrays;
class Solution {
int[][] dp = new int[510][510];
public int dfs(int l, int r, String s){
if(l >= r) return 0;
if(dp[l][r] != -1) return dp[l][r];
// 递归
if(s.charAt(l) == s.charAt(r))
dp[l][r] = dfs(l+1, r-1, s); // 由下一个dfs算出来的值就是 dp[l+1][r-1],然后再给回现在的 dp[l][r]
else
dp[l][r] = Math.min(dfs(l+1, r, s), dfs(l, r-1, s)) + 1;
return dp[l][r];
}
public int minInsertions(String s) {
for(int[] x : dp)
Arrays.fill(x, -1); // Arrays.fill只能给一维数组赋值
int n = s.length();
return dfs(0, n-1, s);
}
}