这周周日的周赛,大家强的离谱,一堆人AK,完了完了,掉分之旅。最后一题主要是思维题,即分析好情况,其实很容易解决,奈何等我有想法的时候,已经快要结束没时间写代码了,蓝瘦。
第一题:模拟(或者python调用函数库)。
第二题:DFS 或者 并查集。
第三题:暴力枚举。
第四题:思维 + 贪心。
详细题解如下。
1.日期之间隔几天(Number Of Days Between Two Dates)
AC代码(C++)
2. 验证二叉树(Validate Binary Tree Nodes)
AC代码(方法一 DFS C++)
AC代码(方法二 并查集 C++)
3.最接近的因数(Closest Divisors)
AC代码(C++)
4.形成三的最大倍数(Largest Multiple of Three)
AC代码(C++)
LeetCode第177场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-177
https://leetcode-cn.com/problems/number-of-days-between-two-dates/
请你编写一个程序来计算两个日期之间隔了多少天。
日期以字符串形式给出,格式为
YYYY-MM-DD
,如示例所示。示例 1:
输入:date1 = "2019-06-29", date2 = "2019-06-30" 输出:1
示例 2:
输入:date1 = "2020-01-15", date2 = "2019-12-31" 输出:15
提示:
- 给定的日期是
1971
年到2100
年之间的有效日期
模拟,我们先找出两个年份中的最小年份
然后分别计算,两个日期到距离这一年有多少天,然后两个天数作差的绝对值就是答案
几个要点1)某个日期,先计算年份,隔了那几年,注意要判断某一年可能是闰年,闰年是 366天,非闰年是 265天。2)计算月份,算已经过了几个月了,加上这些月的天数(注意闰年的二月是 29 天)。3)最后再加上 日即可。
这样子就算出了一个日期到某一年(两个日期中的最小年)的天数
(如果用python,是有一个函数库可以调用的,就很快解决这个问题)
class Solution {
public:
int days[12] = {31, 28, 31, 30, 31,30, 31, 31,30,31,30,31};
int daysBetweenDates(string date1, string date2) {
int y1, m1, d1, y2, m2, d2;
// 先变为数字
y1 = (date1[0] - '0') * 1000 + (date1[1] - '0') * 100 + (date1[2] - '0') * 10 + (date1[3] - '0');
m1 = (date1[5] - '0') * 10 + (date1[6] - '0');
d1 = (date1[8] - '0') * 10 + (date1[9] - '0');
y2 = (date2[0] - '0') * 1000 + (date2[1] - '0') * 100 + (date2[2] - '0') * 10 + (date2[3] - '0');
m2 = (date2[5] - '0') * 10 + (date2[6] - '0');
d2 = (date2[8] - '0') * 10 + (date2[9] - '0');
int tep = min(y1, y2);
int v1 = 0, v2 = 0;
// 计算第一个 日期 到 tep 的天数
// 先是年
for(int i = tep; i < y1; ++i){
if((i % 4 == 0 && i %100 != 0) || i % 400 == 0) v1 += 366;
else v1 += 365;
}
// 月
for(int i = 1;i < m1; ++i) {
if(i == 2) {
if((y1 % 4 == 0 && y1 %100 != 0) || y1 % 400 == 0) v1 += 29;
else v1 += 28;
}
else {
v1 += days[i - 1];
}
}
// 日
v1 += d1;
// 下面类似
for(int i = tep; i < y2; ++i){
if((i % 4 == 0 && i %100 != 0) || i % 400 == 0) v2 += 366;
else v2 += 365;
}
for(int i = 1;i < m2; ++i) {
if(i == 2) {
if((y2 % 4 == 0 && y2 %100 != 0) || y2 % 400 == 0) v2 += 29;
else v2 += 28;
}
else {
v2 += days[i - 1];
}
}
v2 += d2;
// 最后答案就是,两个日期的差的绝对值
return abs(v1 - v2);
}
};
https://leetcode-cn.com/problems/validate-binary-tree-nodes/
二叉树上有 n 个节点,按从 0 到 n - 1 编号,其中节点 i 的两个子节点分别是 leftChild[i] 和 rightChild[i]。
只有 所有 节点能够形成且 只 形成 一颗 有效的二叉树时,返回 true;否则返回 false。
如果节点 i 没有左子节点,那么 leftChild[i] 就等于 -1。右子节点也符合该规则。
注意:节点没有值,本问题中仅仅使用节点编号。
示例 :所有示例具体看链接,有图示
提示:
1 <= n <= 10^4
leftChild.length == rightChild.length == n
-1 <= leftChild[i], rightChild[i] <= n - 1
方法一、DFS
根据题目要求的,返回 true的要求是,一个节点只能是一条子树的,同时,只存在一个二叉树,不能有多个。
因此想法,就是,利用DFS,从根节点 (0) 出发,走这颗树的所有节点
1)如果发现已经有节点被走过了(说明这个节点,不是一个子树的,也就是 示例 2 ,示例3 出现的情况),那就是 false
2)如果从根节点 0 走完了这棵树,还发现还有点没有走(那就是说明,不单单一颗树,也就是示例 4 出现的情况),也是 false
方法二、并查集
方法一的要求是,我们知道某一个根节点(因为要开始走,不能从叶节点开始走,这样子会误判),所以这就要求,0 一定是 根节点(虽然题目没说,但是示例是这样子的,因此方法一在这道题也是可以通过的)。
但是这样子不完善,假如出现数据,0不是根节点,那方法一是会误判的额。
因此还可以利用并查集,也就是将节点归属不同的集合。
1)出现环,或者,一个节点被不同子树链接(也就是示例 2 和 示例 3)。那么这就会出现,本来我们应该是让当前节点 i,找到它的 左右节点 l 和 r,那这几个节点本来应该是同一个集合的。
如果一开始,左右节点 l 和 r 的并查集父节点和 i 的父节点不同(说明这个节点是才刚刚走到,也就是前面还没走过),同时把这两个子节点的父节点和 i 的父节点归属,表示同一个集合。
如果发现左右节点 l 和 r 的某个节点的并查集父节点和 i 的父节点 已经是相同了,说明 左右节点 l 和 r 中的某个 已经被走过了,那就是存在环,或者节点被不同子树链接的情况(也就是 示例 2 ,示例3 出现的情况),那就是 false。
2)最后判断所有集合个数(也就是二叉树的个数),如果数量是 1,那就是 true,数量 > 1,说明不止一个二叉树(也就是示例 4 出现的情况),也是 false。
利用并查集的方法,就不会要求,0 一定是根节点的情况。
并查集的具体学习,可以查看我的另一篇博客:并查集 详细介绍
class Solution {
public:
int vis[10010];
bool flag;
int N;
void dfs(int x, vector& leftChild, vector& rightChild){
if(flag == false) return;
vis[x] = 1;
int l = leftChild[x];
int r = rightChild[x];
if(l != -1) {
if(vis[l] == 1) {
flag = false;
return;
}
dfs(l, leftChild, rightChild);
}
if(r != -1) {
if(vis[r] == 1) {
flag = false;
return;
}
dfs(r, leftChild, rightChild);
}
}
bool validateBinaryTreeNodes(int n, vector& leftChild, vector& rightChild) {
N = n;
memset(vis, 0, sizeof(vis));
flag = true;
dfs(0, leftChild, rightChild);
// 如果走过一遍DFS后,发现是true,最后就判断,是不是只有一个二叉树
if(flag){
for(int i = 0;i < n; ++i) {
if(vis[i] == 0){ // 如果还有没走过的,说明不止一个二叉树
flag = false;
break;
}
}
}
return flag;
}
};
const int MAXN = 1e4 + 10;
class Solution {
public:
int fa[MAXN];
// 找父节点
int findFa(int x) {
if(fa[x] != x)
fa[x] = findFa(fa[x]);
return fa[x];
}
// 将两个合成一个集合
void Unio(int x, int y) {
x = findFa(x);
y = findFa(y);
if(x != y){
fa[y] = x;
}
}
bool validateBinaryTreeNodes(int n, vector& leftChild, vector& rightChild) {
for(int i = 0;i < n; ++i) fa[i] = i;
for(int i = 0;i < n; ++i) {
int l = leftChild[i], r = rightChild[i];
if(l != -1) {
if(findFa(l) == findFa(i)) return false; // 如果已经出现 == ,说明这个点之前走过,那就是 false
Unio(l, i); // 不然两个结合成一个集合
}
if(r != -1) {
if(findFa(r) == findFa(i)) return false;
Unio(r, i);
}
}
// 计算集合的个数
int cnt = 0;
for(int i = 0;i < n; ++i)
if(findFa(i) == i)
++cnt;
return cnt == 1;
}
};
https://leetcode-cn.com/problems/closest-divisors/
给你一个整数 num,请你找出同时满足下面全部要求的两个整数:
- 两数乘积等于 num + 1 或 num + 2
- 以绝对差进行度量,两数大小最接近
你可以按任意顺序返回这两个整数。
示例 1:
输入:num = 8 输出:[3,3] 解释:对于 num + 1 = 9,最接近的两个因数是 3 & 3;对于 num + 2 = 10, 最接近的两个因数是 2 & 5,因此返回 3 & 3 。
示例 2:
输入:num = 123 输出:[5,25]
提示:
- 1 <= num <= 10^9
一开始的想法,就是找到所有的 num + 1 或者 num + 2 的因数,然后找出其中两个因数作差绝对值最小的两个因数,就是答案。
那么根据题目的数据范围,我们找因数的条件是 从 1 到 sqrt(num),那么根据数据范围,我们发现不会超时
所以直接暴力枚举即可。
class Solution {
public:
vector closestDivisors(int num) {
int a, b, diff = 1e9 + 2; // 初始化 diff 为 最大值
int tep = num + 1;
for(int i = 1; i <= (int)sqrt(tep); ++i){
if(tep % i == 0) {
if(diff > abs(tep / i - i)) {
a = i, b = tep / i;
diff = abs(a - b);
}
}
}
tep = num + 2;
for(int i = 1; i <= (int)sqrt(tep); ++i){
if(tep % i == 0) {
if(diff > abs(tep / i - i)) {
a = i, b = tep / i;
diff = abs(a - b);
}
}
}
return {a, b};
}
};
https://leetcode-cn.com/problems/largest-multiple-of-three/
给你一个整数数组 digits,你可以通过按任意顺序连接其中某些数字来形成 3 的倍数,请你返回所能得到的最大的 3 的倍数。
由于答案可能不在整数数据类型范围内,请以字符串形式返回答案。
如果无法得到答案,请返回一个空字符串。
示例 1:
输入:digits = [8,1,9] 输出:"981"
示例 2:
输入:digits = [8,6,7,1,0] 输出:"8760"
示例 3:
输入:digits = [1] 输出:""
示例 4:
输入:digits = [0,0,0,0,0,0] 输出:"0"
提示:
1 <= digits.length <= 10^4
0 <= digits[i] <= 9
- 返回的结果不应包含不必要的前导零。
这道题,是一道有关数学的思维题目:
首先,要懂几个情况。
1)最大数,即,我们将出现的所有 0 - 9 的数字进行降序排序,即大的放在前面,小的放在后面,这样子出来的数,就是最大数。
2)3的倍数:数字的每一位之和,如果是 3 的倍数,那么这个数字也就是 3 的倍数
所以,我们先对数字的每一位求和(也就是 digits 数组中的元素之和,为了验证 3 的倍数
),同时对 digits 数组中出现的额 0 - 9 的每一个数字出现的次数进行统计(为了后面输出最大数)。
这里要明白一个东西
对于求和 % 3 的余数 是 1,我们就要把影响的元素去掉。有两种可能:1)会出现一个数,本身就是 % 3 = 1,那就直接删除这个数即可。2)如果都没有本身就是 % 3 = 1的数。那我们不可能删除 % 3 = 0 的数(本身就是 3 的倍数)。所以就考虑删除 % 3 = 2 的数,那么要删除掉 两个 余数 为 2 的数(这两个余数为2 的数相加,也就是余数为 1)。
对于求和 % 3 的余数 是 2的,我们分析也是,1)直接删除一个% 3 = 2的数。2)没有的话,删除两个 % 3 = 1 的数。
对于我们删除,为了使得结果最大
1)能删除一个数,就不删除两个数
2)删除的数,从余数满足要求的数中最小的删,保证最大
余数为 1的数,可能有 1, 4, 7。余数为 2的数,可能有 2,5,8。(发现了也就是余数为 1,或者 2,就是以 1 或者 2开头,隔 3 位)。
所以整个程序流程:
1)统计 digits
数组中每个元素(0 - 9) 的出现次数,同时求 digits 数组之和。
2)判断余数是 1 还是 2。
如果是 1,先考虑删除一个余数为1。如果还是余数1,(说明没有余数为 1 的数可以删除),那就删除两个 余数为 2 的数。
如果是 2,先考虑删除一个余数为2。如果还是余数2,(说明没有余数为 2 的数可以删除),那就删除两个 余数为 1 的数。
3)最后得到满足要求(是 3 的倍数)的各个数字的保留情况(前面统计了出现次数,对应删除的那就少掉几次)。
4)为了是最大数,我们从 9 开始 到 1(0比较特殊,后面再考虑),每个元素出现多少次,这个元素就放在前面几位,比如 9 出现 2次,1 出现 3 次,那么得到的结果是 99111。
5)最后考虑 0。如果我们前面发现得到的字符串还是 空字符串(也就是没有 9 - 1) 的数字,同时,发现 0的个数有,那么无论有几个 0,我们输出的都是 "0"。如果发现字符串不是空的,也就是前面不是前导零,那么有几个 0 ,就加在字符串后面。
class Solution {
public:
unordered_map mp;
int sum = 0;
// 删除余数为 m 的数,也就是 从小的考虑,所以是 m, m + 3, m + 3 + 3 ..
void del(int m){
for(int i = m;i < 9; i += 3){
if(mp[i]){ //有就删除
sum -= i;
--mp[i];
return;
}
}
}
string largestMultipleOfThree(vector& digits) {
mp.clear(), sum = 0;
for(auto d : digits){
sum += d;
++mp[d];
}
// cout << sum << endl;
// 删除一个余数为 1的
if(sum % 3 == 1){
del(1);
}
// 还是余数为1,说明上一步没有余数为1的数可以删除。那就删除余数为2 的 2个
if(sum % 3 == 1)
{
del(2), del(2);
}
// 当余数为 2的时候,也是一样的考虑
if(sum % 3 == 2){
del(2);
}
if(sum % 3 == 2) {
del(1), del(1);
}
// 最后就是结果
string res = "";
for(int i = 9;i >= 1; --i){
// 从 9 开始,这个元素有几个,就加几个
while(mp[i] != 0){
char tep = i + '0';
res += tep;
--mp[i];
}
}
// 如果还是空字符串,同时 0 又有个数,那就是返回 0 (不然会有前导零)
if(res.size() == 0 && mp[0] > 0) return "0";
if(res.size() != 0)
{
while(mp[0] != 0){
res += '0';
--mp[0];
}
}
return res;
}
};