这周虽然AK了,但感觉不太满意,其实题目都不难,脑袋不清醒,不然可以更快,再接再厉吧。
这周的题,总体上,两道都是关于树的遍历,所以其实也不算很难。主要花时间思考的就是第二题,只要知道思路,代码其实很简单。我自己做的时候,第二题看了之后,一时没想法,做完第三,第四题后,才返回来做第二题的。
第一题:模拟 + 构造。
第二题:思维。
第三题:树的遍历 DFS 或者 BFS。
第四题:树的遍历 DFS。
详细题解如下。
1.生成每种字符都是奇数个的字符串(Generate A String With Characters That Have Odd Counts)
2. 灯泡开关 III(Bulb Switcher III)
3.通知所有员工所需的时间(Time Needed To Inform All Employees)
4.T 秒后青蛙的位置(Frog Position After T Seconds)
LeetCode第179场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-179
1.生成每种字符都是奇数个的字符串(Generate A String With Characters That Have Odd Counts)
题目链接
https://leetcode-cn.com/problems/generate-a-string-with-characters-that-have-odd-counts/
题意
给你一个整数 n,请你返回一个含 n 个字符的字符串,其中每种字符在该字符串中都恰好出现 奇数次 。
返回的字符串必须只含小写英文字母。如果存在多个满足题目要求的字符串,则返回其中任意一个即可。
示例 1:
输入:n = 4 输出:"pppz" 解释:"pppz" 是一个满足题目要求的字符串,因为 'p' 出现 3 次,且 'z' 出现 1 次。当然,还有很多其他字符串也满足题目要求,比如:"ohhh" 和 "love"。
提示:
- 1 <= n <= 500
解题思路
这道题很简单,不要受到示例的影响。其实就是,要求构造的字符串中,出现的那些字母,次数都是奇数。
那么当 n 为奇数的时候,我们直接全部同一个字母,就满足条件。
当 n 为偶数的时候,那就把 n - 1 (奇数)同一个字母,然后剩下的 单独一个,就其他字母。
AC代码(C++)
class Solution {
public:
string generateTheString(int n) {
string res = "";
if(n % 2 == 1)
{
for(int i = 0;i < n; ++i) res += 'a';
}
else
{
for(int i = 0;i < n - 1; ++i) res += 'a';
res += 'b';
}
return res;
}
};
2. 灯泡开关 III(Bulb Switcher III)
题目链接
https://leetcode-cn.com/problems/bulb-switcher-iii/
题意
房间中有 n 枚灯泡,编号从 1 到 n,自左向右排成一排。最初,所有的灯都是关着的。
在 k 时刻( k 的取值范围是 0 到 n - 1),我们打开 light[k] 这个灯。
灯的颜色要想 变成蓝色 就必须同时满足下面两个条件:
- 灯处于打开状态。
- 排在它之前(左侧)的所有灯也都处于打开状态。
请返回能够让 所有开着的 灯都 变成蓝色 的时刻 数目 。
示例 1:
示例有图,具体看链接 输入:light = [2,1,3,5,4] 输出:3 解释:所有开着的灯都变蓝的时刻分别是 1,2 和 4 。
示例 2:
输入:light = [4,1,2,3] 输出:1 解释:所有开着的灯都变蓝的时刻是 3(index-0)。 第 4 个灯在时刻 3 变蓝。
提示:
n == light.length
1 <= n <= 5 * 10^4
light
是[1, 2, ..., n]
的一个排列。
解题思路
这道题,乍看起来,很复杂,但是,如果能从题目抽象出,具体问什么,那就很容易了。
根据题目,我们分析,如果对于当前 时刻 k ,我们打开了 某个灯的编号 light[ k ],如果要求所有开着的灯,都是变成蓝色的,也就是,应该前面的 k - 1 时刻,刚好把 1 2 3 .. (light[ k ] - 1) 打开了,也就是,在当前 k 时刻,打开的灯编号应该是 1 2 3 4 .... light[ k ]
方法一、利用编号求和
如果当前所有灯都是蓝色,那么亮灯的编号和必定等于1+2+...+亮灯数目,
因此,我们维护,此时前面亮灯的编号和,和如果要是都蓝色的情况下,当前理论上的亮灯编号和。
也就是,一个值 cur ,是不断求,根据时刻 k 来亮灯的编号
一个值 sum ,是求 1 + 2 + 3 + ... + 亮灯数(即 时刻+ 1).
如果两个值,相等,说明这个时刻的亮着的灯,都变成了蓝色。
(如果不相等,说明这个时候的亮灯编号前,有一个灯没亮,亮的是后面的灯,那么求出来的 cur ,就不会等于 sum)
方法二、维护最大值
也就是,当前时刻 k,本来的编号最大值是 k + 1,这样子才是蓝色(因为对于时刻 k,应该都是亮 1 2 3 ... k+1 ),然后不断的更新编号最大值,判断如果 == ,那么就是这个时刻是蓝色的。
AC代码(方法一、C++)
class Solution {
public:
int numTimesAllBlue(vector<int>& light) {
int n = light.size();
int res = 0;
int sum = 0;
int cur = 0;
for(int i = 0;i < n; ++i)
{
sum += (i + 1);
cur += light[i];
if(sum == cur) ++res;
}
return res;
}
};
AC代码(方法二、C++)
class Solution {
public:
int numTimesAllBlue(vector<int>& light) {
int n = light.size();
int res = 0;
int mx = 0;
for(int i = 0;i < n; ++i)
{
mx = max(mx, light[i]);
if(mx == i + 1) ++res;
}
return res;
}
};
3.通知所有员工所需的时间(Time Needed To Inform All Employees)
题目链接
https://leetcode-cn.com/problems/time-needed-to-inform-all-employees/
题意
公司里有 n 名员工,每个员工的 ID 都是独一无二的,编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。
在 manager 数组中,每个员工都有一个直属负责人,其中 manager[i] 是第 i 名员工的直属负责人。对于总负责人,manager[headID] = -1。题目保证从属关系可以用树结构显示。
公司总负责人想要向公司所有员工通告一条紧急消息。他将会首先通知他的直属下属们,然后由这些下属通知他们的下属,直到所有的员工都得知这条紧急消息。
第 i 名员工需要 informTime[i] 分钟来通知它的所有直属下属(也就是说在 informTime[i] 分钟后,他的所有直属下属都可以开始传播这一消息)。
返回通知所有员工这一紧急消息所需要的 分钟数
示例 2:
示例有图,具体看链接 输入:n = 6, headID = 2, manager = [2,2,-1,2,2,2], informTime = [0,0,1,0,0,0] 输出:1 解释:id = 2 的员工是公司的总负责人,也是其他所有员工的直属负责人,他需要 1 分钟来通知所有员工。 上图显示了公司员工的树结构。
示例 3:
输入:n = 7, headID = 6, manager = [1,2,3,4,5,6,-1], informTime = [0,6,5,4,3,2,1] 输出:21 解释:总负责人 id = 6。他将在 1 分钟内通知 id = 5 的员工。 id = 5 的员工将在 2 分钟内通知 id = 4 的员工。 id = 4 的员工将在 3 分钟内通知 id = 3 的员工。 id = 3 的员工将在 4 分钟内通知 id = 2 的员工。 id = 2 的员工将在 5 分钟内通知 id = 1 的员工。 id = 1 的员工将在 6 分钟内通知 id = 0 的员工。 所需时间 = 1 + 2 + 3 + 4 + 5 + 6 = 21 。
提示:
- 1 <= n <= 10^5
- 0 <= headID < n
- manager.length == n
- 0 <= manager[i] < n
- manager[headID] == -1
- informTime.length == n
- 0 <= informTime[i] <= 1000
- 如果员工 i 没有下属,informTime[i] == 0 。
- 题目 保证 所有员工都可以收到通知。
解题分析
根据题目意思,再结合示例的说明,发现就是将从属关系,变为一棵树(有向树),同时通知时间,相当于是边的长度。
那么这道题就是,要求出,根节点,到所有叶节点花费的时间的最大值(也就是树的遍历)
树的遍历(有向树),DFS 和 BFS 都可以使用
无论是,DFS还是BFS,实际上都是每个点,都遍历了一次,时间复杂度和空间复杂度,都是 O(N)
AC代码(方法一、DFS C++)
const int MAXN = 1e5 + 10;
class Solution {
public:
vector<int> G[MAXN];
int ans = 0;
void dfs(int x, int n, vector<int>& informTime, int sum)
{
if(G[x].size() == 0)
{
ans = max(ans, sum); // 当前点没有可以往下了,说明,这个点就是叶节点,那么更新总时间
return;
}
for(int i = 0;i < G[x].size(); ++i) // 继续 DFS 下一个点
{
dfs(G[x][i], n, informTime, sum + informTime[x]);
}
}
int numOfMinutes(int n, int headID, vector<int>& manager, vector<int>& informTime) {
for(int i = 0;i < n; ++i)
{
if(manager[i] == -1) continue;
G[manager[i]].push_back(i);
}
ans = 0;
dfs(headID, n, informTime, 0); // DFS,从当前根节点开始遍历
return ans;
}
};
AC代码(方法二、BFS C++)
const int MAXN = 1e5 + 10;
class Solution {
public:
vector<int> G[MAXN];
int dist[MAXN];
int vis[MAXN];
int numOfMinutes(int n, int headID, vector<int>& manager, vector<int>& informTime) {
// 树的邻接矩阵
for(int i = 0;i < n; ++i)
{
if(manager[i] == -1) continue;
G[manager[i]].push_back(i);
}
memset(vis, 0, sizeof(vis));
memset(dist, 0, sizeof(dist));
queue<int> q; // BFS模板,利用队列
while(!q.empty()) q.pop();
int ans = 0;
q.push(headID);
vis[headID] = 1;
dist[headID] = 0;
while(!q.empty())
{
int x = q.front();
q.pop();
ans = max(ans, dist[x]); // 每次都取,根节点到当前节点的花费时间最大值
for(int i = 0;i < G[x].size(); ++i) // 从 x 可以到 其他的点
{
int y = G[x][i];
if(vis[y] == 1) continue;
vis[y] = 1;
dist[y] = dist[x] + informTime[x]; // x 到 y,那么 y 的时间,就是 x 的时间,再加上 x 通知 y 的时间
q.push(y);
}
}
return ans;
}
};
4.T 秒后青蛙的位置(Frog Position After T Seconds)
题目链接
https://leetcode-cn.com/problems/frog-position-after-t-seconds/
题意
给你一棵由 n 个顶点组成的无向树,顶点编号从 1 到 n。青蛙从 顶点 1 开始起跳。规则如下:
- 在一秒内,青蛙从它所在的当前顶点跳到另一个 未访问 过的顶点(如果它们直接相连)。
- 青蛙无法跳回已经访问过的顶点。
- 如果青蛙可以跳到多个不同顶点,那么它跳到其中任意一个顶点上的机率都相同。
- 如果青蛙不能跳到任何未访问过的顶点上,那么它每次跳跃都会停留在原地。
无向树的边用数组 edges 描述,其中 edges[i] = [fromi, toi] 意味着存在一条直接连通 fromi 和 toi 两个顶点的边。
返回青蛙在 t 秒后位于目标顶点 target 上的概率。
示例 1:
示例有图,具体看链接 输入:n = 7, edges = [[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]], t = 2, target = 4 输出:0.16666666666666666 解释:上图显示了青蛙的跳跃路径。青蛙从顶点 1 起跳,第 1 秒 有 1/3 的概率跳到顶点 2 ,然后第 2 秒 有 1/2 的概率跳到顶点 4,因此青蛙在 2 秒后位于顶点 4 的概率是 1/3 * 1/2 = 1/6 = 0.16666666666666666 。
示例 3:
输入:n = 7, edges = [[1,2],[1,3],[1,7],[2,4],[2,6],[3,5]], t = 20, target = 6 输出:0.16666666666666666
提示:
1 <= n <= 100
edges.length == n-1
edges[i].length == 2
1 <= edges[i][0], edges[i][1] <= n
1 <= t <= 50
1 <= target <= n
与准确值误差在 10^-5 之内的结果将被判定为正确。
解题分析
根据题目,结合示例,发现也是一个树的问题,但是这个问题,是一个无向树(可是由于题目说明了一定是从 1 出发,其实也就是变成了一个单向的了,但是由于题目已经说明是无向树,为了防止数据上设陷阱,我们存的树结构,还是用无向树的邻接矩阵来存储)
也就是,当我们从根节点出发,要求判断,T 秒的时间,出现在指定位置的概率。因为这个有很多种可能达到,所以我们用 DFS,找到所有可能,把概率值求和,就是答案。
那么从某一个点出发,如果它有 cnt 个子节点,那么这个点到达任意一个子节点的概率 = 当前点概率 * (1.0 / cnt)。
那么我们就用DFS从根节点去搜索,用一个值去记录,到达当前节点的概率。
- 假如,这个点就是目标点,同时我们还要考虑:如果它没有子节点了,说明会一直呆在这个点,即使还有时间,也动不了(示例 3);要么即使它还有子节点,但是时间刚好走完了(不会继续往下走了),那么也是会处于目标点。这个时候,就把此时到达目标点的概率累加起来(也就是答案)。那么其他情况,即使到达了目标点,但是由于不会呆在这个点上,会继续向其他点走,也就是此时这条路线的DFS已经没必要继续下去了,就进行剪枝。
- 如果这个达到,这个点,已经时间为 0 了,说明也不用继续往下DFS了
- 剩下的,就表示,还可以继续往下走,也就是找到所有 x 可以走到的 y,那么继续 DFS 的时候:时间 - 1,概率 = x 的概率 * (1.0 / cnt)。(注意这里要回溯,因为可能同一个点,还有其他路线到达,万一有这种情况)
那么总时间复杂度应该是 O(N * T),我们每个点,都遍历一次,同时遍历一次,还要注意 最多 T 的时间
AC代码(C++)
const int MAXN = 115;
class Solution {
public:
int vis[MAXN];
vector<int> G[MAXN];
double ans = 0.0;
void dfs(int x, int t, int target, double val)
{
int cnt = 0;
for(int i = 0;i < G[x].size(); ++i) // 统计这个点,还可以走到几个点
if(vis[G[x][i]] == 0) ++cnt;
if(x == target) // 如果是目标值
{
if(cnt == 0 || t == 0) // 要求是,不能再继续往下移动(不然就无法呆在这个点上),也就是它是叶节点,或者没时间了
{
ans += val;
return;
}
else
return;
}
if(t == 0) return;
// 继续往下走
for(int i = 0;i < G[x].size(); ++i)
{
if(vis[G[x][i]] == 1) continue;
vis[G[x][i]] = 1;
dfs(G[x][i], t - 1, target, val * (1.0 / cnt)); // 概率的更新,还有时间 - 1
vis[G[x][i]] = 0;
}
}
double frogPosition(int n, vector<vector<int>>& edges, int t, int target) {
// 无向图
for(auto e : edges)
{
G[e[0]].push_back(e[1]);
G[e[1]].push_back(e[0]);
}
memset(vis, 0, sizeof(vis));
vis[1] = 1;
dfs(1, t, target, 1.0); // 其实概率是 1
return ans;
}
};