这周状态不错,AK了,可喜可贺。但还是速度慢,同时代码实现能力还需要继续加强。加油吧,骚年。
比赛重要的是速度,而不是最优解,要注意,和平时自己刷题不同,自己刷题,重点是要考虑多种解法,找出最优解。但是比赛的时候,只要在时间复杂度允许的情况下,暴力是最快的。
第一题:暴力枚举 或者 哈希表 + 排序。
第二题:排序(重写 cmp 排序依据)。
第三题:树的DFS。
第四题:0 / 1 最短路(使用了SPFA)或者 双端队列BFS。
详细题解如下。
1.有多少小于当前数字的数字(How Many Numbers Are Smaller Than The Current Number)
AC代码(方法一、暴力枚举 C++)
AC代码(方法二、哈希 + 排序 C++)
2. 通过投票对团队排名(Rank Teams By Votes)
AC代码(C++)
3.二叉树中的列表(Linked List In Binary Tree)
AC代码(C++)
4.使网格图至少有一条有效路径的最小代价(Minimum Cost To Make At Least One Valid Path In A Grid)
AC代码(方法一、SPFA C++)
AC代码(方法二、双端队列BFS C++)
LeetCode第178场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-178/
https://leetcode-cn.com/problems/how-many-numbers-are-smaller-than-the-current-number/
给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。
换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 nums[j] < nums[i] 。
以数组形式返回答案。
示例 1:
输入:nums = [8,1,2,2,3] 输出:[4,0,1,1,3] 解释: 对于 nums[0]=8 存在四个比它小的数字:(1,2,2 和 3)。 对于 nums[1]=1 不存在比它小的数字。 对于 nums[2]=2 存在一个比它小的数字:(1)。 对于 nums[3]=2 存在一个比它小的数字:(1)。 对于 nums[4]=3 存在三个比它小的数字:(1,2 和 2)。
示例 3:
输入:nums = [7,7,7,7] 输出:[0,0,0,0]
提示:
2 <= nums.length <= 500
0 <= nums[i] <= 100
思路一、暴力枚举
看到题目,想到的就是,对于每一个数,枚举其他所有数,进行大小比较。这样子时间复杂度是 O(N ^ 2), 空间复杂度是 O(1)。
根据数据范围,不会超时,所以直接暴力枚举
思路二、哈希表 + 排序
我们可以先对原数组排序(由于这里原数组不能改动,所以要复制一个数组),进行从小到大排序(这样子对于 第 i 个数 ,前面就有 最多 i 个数比该数小),根据 1 2 2,当我们计算 2 的排序,应该是 1, 当判断到 第二个 2 的时候,由于此时 2 已经有顺序了,那么就不重新记。
对于每一个数,我们要快速查找到这个数,前面有几个比它小的,那么就用 哈希表 来记录。
接着遍历每一个数,然后用哈希表就可以快速知道这个数,有几个数比它小了。
时间复杂度是 O(N * logN), 空间复杂度是 O(N),用空间换时间
class Solution {
public:
vector smallerNumbersThanCurrent(vector& nums) {
vector ans;
int n = nums.size();
for(int i = 0;i < n; ++i)
{
int cnt = 0;
for(int j = 0;j < n; ++j)
{
if(j == i) continue;
if(nums[i] > nums[j]) ++cnt;
}
ans.push_back(cnt);
}
return ans;
}
};
class Solution {
public:
unordered_map cnt;
vector smallerNumbersThanCurrent(vector& nums) {
vector ans = nums;
int n = nums.size();
sort(ans.begin(), ans.end());
for(int i = 0;i < n; ++i)
{
if(cnt[ans[i]] != 0) continue;
cnt[ans[i]] = i + 1; // 本来数量 第 0 个是 0,但是这个可能会被重新覆盖,所以就 都加 1,最后数量再 - 1 回来
}
for(int i = 0;i < n; ++i)
{
ans[i] = cnt[nums[i]] - 1;
}
return ans;
}
};
https://leetcode-cn.com/problems/rank-teams-by-votes/
现在有一个特殊的排名系统,依据参赛团队在投票人心中的次序进行排名,每个投票者都需要按从高到低的顺序对参与排名的所有团队进行排位。
排名规则如下:
参赛团队的排名次序依照其所获「排位第一」的票的多少决定。如果存在多个团队并列的情况,将继续考虑其「排位第二」的票的数量。以此类推,直到不再存在并列的情况。
如果在考虑完所有投票情况后仍然出现并列现象,则根据团队字母的字母顺序进行排名。
给你一个字符串数组 votes 代表全体投票者给出的排位情况,请你根据上述排名规则对所有参赛团队进行排名。请你返回能表示按排名系统 排序后 的所有团队排名的字符串。
示例 1:
输入:votes = ["ABC","ACB","ABC","ACB","ACB"] 输出:"ACB" 解释:A 队获得五票「排位第一」,没有其他队获得「排位第一」,所以 A 队排名第一。 B 队获得两票「排位第二」,三票「排位第三」。 C 队获得三票「排位第二」,两票「排位第三」。 由于 C 队「排位第二」的票数较多,所以 C 队排第二,B 队排第三。
示例 2:
输入:votes = ["WXYZ","XYZW"] 输出:"XWYZ" 解释:X 队在并列僵局打破后成为排名第一的团队。X 队和 W 队的「排位第一」票数一样,但是 X 队有一票「排位第二」,而 W 没有获得「排位第二」。
提示:
1 <= votes.length <= 1000
1 <= votes[i].length <= 26
votes[i].length == votes[j].length for 0 <= i, j < votes.length
votes[i][j] 是英文 大写 字母
votes[i] 中的所有字母都是唯一的
votes[0] 中出现的所有字母 同样也 出现在 votes[j] 中,其中 1 <= j < votes.lengt
这道题,主要就是排序(也就是对各个字母进行排序),排序依据是,根据【排位第1】的数量进行排序;如果相同,就依据【排位第2】的数量进行排序 .... 依此到最后。如果最后【排位】的数量都相同,那么此时,对字母的字典序升序进行排序。
因此,我们就要统计,【排位第几】时,【这个字母出现的次数(也就是被投了几票)】,那么我们用一个二维数组即可(26 * 26),因为排位最多有 26 (因为最多 26 个字母),然后有 26 个字母。
然后重写 sort 的 cmp 即可
int cnt[30][30];
int N;
bool cmp(char a, char b) // 排序依据
{
for(int i = 1;i <= N; ++i) // 从排位第 1 .. 一直到 排位 第 N
{
if(cnt[i][a - 'A'] != cnt[i][b - 'A'])
return cnt[i][a - 'A'] > cnt[i][b - 'A'];
}
return a < b; // 都相同,就按字母字典序升序排
}
class Solution {
public:
string rankTeams(vector& votes) {
memset(cnt, 0, sizeof(cnt));
N = votes[0].size();
for(auto v : votes) // 统计次数
{
for(int i = 0;i < v.size(); ++i)
{
++cnt[i + 1][v[i] - 'A'];
}
}
vector zimu; // 出现的所有字母情况
for(int i = 0;i < votes[0].size(); ++i)
{
zimu.push_back(votes[0][i]);
}
sort(zimu.begin(), zimu.end(), cmp); // 对这个字母数组进行排序
string ans = "";
for(int i = 0;i < zimu.size(); ++i) // 得到顺序后,变为 string 输出
ans += zimu[i];
return ans;
}
};
https://leetcode-cn.com/problems/linked-list-in-binary-tree/
给你一棵以 root 为根的二叉树和一个 head 为第一个节点的链表。
如果在二叉树中,存在一条一直向下的路径,且每个点的数值恰好一一对应以 head 为首的链表中每个节点的值,那么请你返回 True ,否则返回 False 。
一直向下的路径的意思是:从树中某个节点开始,一直连续向下的路径。
示例 有图,具体看链接
提示:
- 二叉树和链表中的每个节点的值都满足 1 <= node.val <= 100 。
- 链表包含的节点数目在 1 到 100 之间。
- 二叉树包含的节点数目在 1 到 2500 之间。
首先使用dfs遍历二叉树,找到与链表头节点值相同的节点。
然后在用第二个dfs,开始逐个匹配该链表在不在二叉树中。
如果有满足,那就是答案时 true(对于所有 dfs,只要发现,flag == true,说明已经存在了,那么剩下的 dfs 都不用再遍历了)
class Solution {
public:
vector nums;
int n;
bool flag = false;
void dfsCheck(TreeNode* root, int cnt) // 检查从这个起点开始,是不是有完整的
{
if(root == NULL) return;
// 对于 cnt,是计算,此时已经连续有相等的几个了
// 如果对于 cnt == n - 1 了,说明树中有和链表一样的树了
if(root->val == nums[cnt] && cnt == n - 1)
{
flag = true; // 置为 true
return;
}
if(root->val == nums[cnt]) // 如果对于这个数相等,那么继续往下考虑,下一个节点,是不是和 cnt + 1,相等。
{
dfsCheck(root->left, cnt + 1);
dfsCheck(root->right, cnt + 1);
}
}
void dfsStart(TreeNode* root)
{
if(flag == true) // 如果已经是 true了,那就所有DFS都停止
return;
if(root == NULL) return;
if(root->val == nums[0]) // 找到了这个起点,那么根据这个起点,开始第二层DFS,开始逐个匹配该链表在不在二叉树中。
{
dfsCheck(root, 0);
}
dfsStart(root->left);
dfsStart(root->right);
}
bool isSubPath(ListNode* head, TreeNode* root) {
// 为了方便,我把链表中的数,都保存在 vector 中
while(head != NULL)
{
nums.push_back(head->val);
// cout << head->val << endl;
head = head->next;
}
n = nums.size();
dfsStart(root); // 第一层 dfs,找到与链表头节点值相同的节点
return flag;
}
};
https://leetcode-cn.com/problems/minimum-cost-to-make-at-least-one-valid-path-in-a-grid/
给你一个 m x n 的网格图 grid 。 grid 中每个格子都有一个数字,对应着从该格子出发下一步走的方向。 grid[i][j] 中的数字可能为以下几种情况:
- 1 ,下一步往右走,也就是你会从 grid[i][j] 走到 grid[i][j + 1]
- 2 ,下一步往左走,也就是你会从 grid[i][j] 走到 grid[i][j - 1]
- 3 ,下一步往下走,也就是你会从 grid[i][j] 走到 grid[i + 1][j]
- 4 ,下一步往上走,也就是你会从 grid[i][j] 走到 grid[i - 1][j]
注意网格图中可能会有 无效数字 ,因为它们可能指向 grid 以外的区域。
一开始,你会从最左上角的格子 (0,0) 出发。我们定义一条 有效路径 为从格子 (0,0) 出发,每一步都顺着数字对应方向走,最终在最右下角的格子 (m - 1, n - 1) 结束的路径。有效路径 不需要是最短路径 。
你可以花费 cost = 1 的代价修改一个格子中的数字,但每个格子中的数字 只能修改一次 。
请你返回让网格图至少有一条有效路径的最小代价。
示例 1:
示例有图,具体看链接 输入:grid = [[1,1,1,1],[2,2,2,2],[1,1,1,1],[2,2,2,2]] 输出:3 解释:你将从点 (0, 0) 出发。 到达 (3, 3) 的路径为: (0, 0) --> (0, 1) --> (0, 2) --> (0, 3) 花费代价 cost = 1 使方向向下 --> (1, 3) --> (1, 2) --> (1, 1) --> (1, 0) 花费代价 cost = 1 使方向向下 --> (2, 0) --> (2, 1) --> (2, 2) --> (2, 3) 花费代价 cost = 1 使方向向下 --> (3, 3) 总花费为 cost = 3.
示例 2:
示例有图,具体看链接 输入:grid = [[1,1,3],[3,2,2],[1,1,4]] 输出:0 解释:不修改任何数字你就可以从 (0, 0) 到达 (2, 2) 。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 100
由于对于一个位置,可能有四个方向可以走,因此考虑 DFS 的话,那么时间复杂度大概是 4 ^ (m * n) 就会超时
思路一、最短路SPFA
所以要好好思路,对于此时的一个位置,它可以走到四个方向,其中一个方向的cost = 0,其他四个方向的 cost = 1,那么如果我们记录 dist[ x ][ y ] 表示 (0, 0) 到 (x, y) 的最小 cost,那么当我们从一个位置,走到其他位置的时候,更新此时的 dist,这就有点,类似,最短路的问题。
如果我们把 cost 看成是 路径,那么 dist[ x ][ y ] 表示 (0, 0) 到 (x, y) 的最小 距离。
对于每一个位置 (x, y) ,我们走的四个方向中,一个方向是路径为0,其他三个方向路径为 1。可以从 (x, y) 走到了 (tx, ty),那么对于 dist[ tx ][ ty ] 的距离,就要取,是这个距离短,还是 dsit[ x ][ y ] + cost 更短。
因此就是一个最短路的问题,我使用的是,SPFA求最短路(正权 或 0 都可以用)。
那么最后的输出就是 dist[ n - 1][ m - 1 ]
思路二、双端队列BFS
对于SPFA方法,数据范围不会卡,但是如果数据范围大点,那么对于SPFA,可能会超时(因为SPFA,每一个点,可能不止走一次,可能会多次)。
那么我们对于 0 / 1 最短路,本来最快的应该是 BFS,那么对于这道题,也是可以使用的,因为BFS,对于每一个点,只会走一次(那么时间复杂度不会太大)。
也就是当我们 考虑某一个点(如果这个点前面已经考虑过了,就不考虑)的时候,它会有两种情况:
1)不需要 cost 的转移
2)需要 cost = 1 的转移
转移过后,我们更新最短距离,那么从 (x, y) 到 (tx, ty),对于用 0 cost 转移的,说明这个点,能更快到,优先考虑最短的,这样子才能去优先更新其他点,说明对于这个点,要优先考虑。对于 1 cost 转移的,就是正常的顺序即可。
因此,这里对 BFS 的顺序,会有两种情况,因此考虑使用 双端列队的 BFS
当发现是 0 cost的情况,那么 (tx, ty)这个点,放到队列前面,表示这个点,要优先考虑(因为这个点是 0
cost 过来的,优先更短的)。其他的情况,就按正常顺序,放在后面即可
正常的BFS,一定是权值为 1 的(相当于是树的边连),那么此时用队列,存的时候
x x x ... x (x + 1) (x + 1) (x + 1) (x + 1)
我们从队头,拿出了最短的 x,更新其他的距离,由于权值为 1,所以后面的距离是 (x + 1),那么放到 队尾 的 时候,还是保证了整个队列是满足,距离从小到大的。
但是由于这道题,是 0 / 1 ,也就是说,当我们拿到 x 的时候,可能更新的距离是 x,也可能是 (x + 1),如果直接都放到队尾,就导致了,队列不能满足,距离从小到大(最短路)。
所以我们用 双端队列 来解决,也就是说,当我们从队列头 拿到 x 的时候,去更新距离。当发现还是 x ,那我们就放到队列头。如果发现是 (x+ 1) 就放到队列尾。
这样子就保证了整个队列还是满足,从小到大的。
const int MAXN = 150;
#define INF 0x3f3f3f3f
class Solution {
public:
int dist[MAXN][MAXN]; // 从 0,0 到 i, j 的最短路
int inQue[MAXN][MAXN]; // SPFA模板
int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
int minCost(vector>& grid) {
int n = grid.size(), m = grid[0].size();
queue> que; // 存入的是,一个点坐标
while(!que.empty()) que.pop();
for(int i = 0;i < n;++i)
for(int j = 0;j < m; ++j)
dist[i][j] = INF;
dist[0][0] = 0; // 初始化
inQue[0][0] = 1; // 表示这个点,在队列中
que.push(make_pair(0, 0));
while(!que.empty())
{
int x = que.front().first, y = que.front().second;
que.pop();
inQue[x][y] = 0;
for(int i = 1;i <= 4; ++i) // 四个方向
{
int tx = x + dx[i - 1], ty = y + dy[i - 1]; // 我们的 dx 是从 0 到 3,所以是 i - 1
if(tx < 0 || tx >= n || ty < 0 || ty >= m) continue;
if(i == grid[x][y]) // 其中一个方向的 cost = 0
{
if(dist[tx][ty] > dist[x][y]) // 要更新距离
{
dist[tx][ty] = dist[x][y];
if(!inQue[tx][ty]) // 这个点不在队列,因此,才要重新加进去考虑,不然已经在队列了,后面自然会考虑上
{
inQue[tx][ty] = 1;
que.push(make_pair(tx, ty));
}
}
}
else
{
// 另外三个方向的 cost = 1
if(dist[tx][ty] > dist[x][y] + 1)
{
dist[tx][ty] = dist[x][y] + 1;
if(!inQue[tx][ty])
{
inQue[tx][ty] = 1;
que.push(make_pair(tx, ty));
}
}
}
}
}
return dist[n - 1][m - 1];
}
};
const int MAXN = 150;
#define INF 0x3f3f3f3f
class Solution {
public:
int dist[MAXN][MAXN]; // 从 0,0 到 i, j 的最短路
int vis[MAXN][MAXN]; // SPFA模板
int dx[4] = {0, 0, 1, -1}, dy[4] = {1, -1, 0, 0};
int minCost(vector>& grid) {
int n = grid.size(), m = grid[0].size();
deque> que; // 存入的是,一个点坐标
while(!que.empty()) que.pop_front();
for(int i = 0;i < n;++i)
for(int j = 0;j < m; ++j)
dist[i][j] = INF;
dist[0][0] = 0; // 初始化
que.push_back(make_pair(0, 0));
while(!que.empty())
{
int x = que.front().first, y = que.front().second;
que.pop_front();
// 如果这个点已经判断过了,那就不用判断了
if(vis[x][y] == 1) continue;
vis[x][y] = 1;
for(int i = 1;i <= 4; ++i) // 四个方向
{
int tx = x + dx[i - 1], ty = y + dy[i - 1]; // 我们的 dx 是从 0 到 3,所以是 i - 1
if(tx < 0 || tx >= n || ty < 0 || ty >= m) continue;
if(i == grid[x][y]) // 其中一个方向的 cost = 0
{
if(dist[tx][ty] > dist[x][y]) // 要更新距离
{
dist[tx][ty] = dist[x][y];
que.push_front(make_pair(tx, ty)); // 因为这个点,是直接 0 cost 过来的,所以这个点,要优先考虑,所以放在队列前(优先判断)
}
}
else
{
// 另外三个方向的 cost = 1
if(dist[tx][ty] > dist[x][y] + 1)
{
dist[tx][ty] = dist[x][y] + 1;
que.push_back(make_pair(tx, ty)); // 这个点,就和正常的BFS一样即可
}
}
}
}
return dist[n - 1][m - 1];
}
};