LeetCode 每日一题
每日一题
2021年3月30日
74. 搜索二维矩阵
数据量小的可怜
我实现的是官方题解的方法二
嘛,和一没什么区别实话实说。
二分没什么可说的,理解一下就好。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int n=matrix.size();
int m=matrix[0].size();
int l=0,r=n*m-1;
while(l<r){
int mid=(r+l)>>1;
if(matrix[mid/m][mid%m]<target)
l=mid+1;
else
r=mid;
}
return matrix[l/m][l%m]==target;
}
};
2021年3月31日
90. 子集 II
很有意思的一个题目。嗯,在一定程度上。
首先,你需要学会怎么遍历所有子集。
其实就是对数组里每一位进行选或不选。
之后,我们再考虑重复问题
首先我们先进行排序,然后进行简单的遍历,就可以得到下面的结果
1 2 2
1 2
1 2
2
2
我们观察上面遍历出来的几个子集
可以发现,怎样选才会重复。
对于在集合里重复出现的数,如果之前相同的数没有选。
那么显然,此次递归可以直接返回
即,当我们递归到:
1,2时
对于2我们在递归里是有两步操作的
-
选,那么答案添加一个1,2
-
不选,迭代到下一个2。我们可以发现,它先前重复出现的数字,我们没有选择。而如果选择此次的2,那么便和上面①重复。
由此我们便可以得到去重的方式了。
class Solution {
private:
bool vis[11];
vector<vector<int>>ans;
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
dfs(nums,0,nums.size());
return ans;
}
void dfs(vector<int> v,int dep,int n){
if(dep==n){
vector<int>tmp;
for(int i=0;i<n;i++)
if(vis[i])
tmp.push_back(v[i]);
ans.push_back(tmp);
return ;
}
vis[dep]=0;
dfs(v,dep+1,n);
if(dep>0&&!vis[dep-1]&&v[dep-1]==v[dep])
return ;
vis[dep]=1;
dfs(v,dep+1,n);
}
};
2021年4月1日
1006. 笨阶乘
两个法,数学法和暴力法
阶乘就要有阶乘的样子
观察一下样例
我们可以对其简单的进行分组。最开始的\(10*9/8\)是一组,然后\(7\),\(-6*5/4\),然后又是\(3\),然后又是\(2*1\)
那么规律就很简单的出来了,定义一个\(x\),记录层数,偶数层就是\(-N*(N-1)/(N-2)\),奇数层就是\(N\)
然后在第\(0\)层和偶数层&&N=2时进行特判
能跑184ms的阶乘就写出来了§( ̄▽ ̄)§
class Solution {
public:
int fun(int x,int N){
if(N==1||N==0){
if(x%2||x==0)
return N;
return -N;
}
int ans=0;
if(x%2)
ans=N+fun(x+1,N-1);
else{
if(x==0)
ans= N*(N-1);
else
ans= -N*(N-1);
if(N!=2)
ans=ans/(N-2)+fun(x+1,N-3);
}
return ans;
}
int clumsy(int N) {
return fun(0,N);
}
};
数学永远滴神
我们对\(N*(N-1)/(N-2)\)进行整理
我们可以得到这么一个式子
我们将关注点放在\(\lfloor\frac{2}{N-2}\rfloor\)上
于是我们将重点放在\(N>4\)
对于笨阶乘,我们的公式如下
可以知道除了第一串,后面相当于两两相减
将上面的结论带到这里,就得到下面的式子
于是,我们只需要关注\((N-3)\bmod4\)所得到的值即可
class Solution {
public:
int clumsy(int N) {
if(N==4)
return 7;
if(N==3)
return 6;
if(N==2||N==1)
return N;
switch((N-3)%4){
case 0:
return N-1;
case 1:
return N+1;
default:
return N+2;
}
}
};
2021年4月2日
面试题 17.21. 直方图的水量
搭眼一看就是个单调栈的板子题。
然后写了半天没写出来XD
我们从左往右扫一遍,过程中维护一个单调递减的单调栈,其中保存下标。
我们考虑下面这张图
序列是\([3,2,1,4]\),首先,单调栈stack里存储的是[3,2,1]。
当我们扫到4时,发现4>1
那么我们的长就是4的下标-栈中1左边下标
高就是min(4,2)-1
我们便得到了红色矩形的面积
我们将1从栈中弹出,我们发现4>2
于是重复上面的操作
长度=4的下标4-左边的下标1
高度=min(4,3)-2
于是我们得到了蓝色矩阵的面积
将两个矩阵的面积相加,就是我们的结果。
然后对于高度3,我们发现它左边莫的东西,就直接将其弹出即可。
class Solution {
public:
int trap(vector<int>& height) {
stack<int>st;
int ans=0,n=height.size();
for(int i=0;i<n;i++){
while(!st.empty()&&height[st.top()]<height[i]){
int mid=st.top();
st.pop();
if(st.empty())
break;
int l=st.top();
int w=i-l-1;
int h=min(height[i],height[l])-height[mid];
ans+=h*w;
}
st.push(i);
}
return ans;
}
};
2021年4月3日
1143. 最长公共子序列
一个题目难的往往不是看题解,而是想解法
虽然有些题目,解法都看不懂
动态规划入门题,很好理解
对于两个字符串
-
当s[i]=t[j]的时候,我们的将这个字母加上,从上个步骤转移,dp[i][j]=dp[i-1][j-1]+1
-
当s[i]!=t[j]的时候,我们上一步只有两个,一个是dp[i][j-1],一个是dp[i-1][j]。
- 什么意思?
- 在这种情况下,影响我们答案的,只有两种方式,一种是在后面加上s的一个字符,一种是在后面加上t的一个字符。
手动模拟一下这张图就明白了
class Solution {
int dp[1000+10][1000+10];
public:
int longestCommonSubsequence(string text1, string text2) {
int len1=text1.length(),len2=text2.length();
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(text1[i-1]==text2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[len1][len2];
}
};
1044. 最长重复子串
做最长公共子序列的时候突然想到的题目,就一块贴上来了。
后缀数组跑一遍,然后取最大height即可。
const int MAXN=1e5+10;
class Solution {
private:
string s;
int rk[MAXN], sa[MAXN], tax[MAXN], hg[MAXN], n, m;
int tp[MAXN];
//tp第二关键字数组
//rk第一关键字第i位的排名,sa排名第i位的位置
void rsort(int a[], int b[]) { //a第一关键字数组,b第二关键字数组
for (int i = 0; i <= m; i++) //桶清零
tax[i] = 0;
for (int i = 1; i <= n; i++) //在第一关键字相同的情况下,按照第二关键字从小到大放入
tax[a[b[i]]]++;
for (int i = 1; i <= m; i++) //计算排名
tax[i] += tax[i - 1];
for (int i = n; i >= 1; i--) //在第一关键字相同的情况下,按照第二关键字从大到小取出
sa[tax[a[b[i]]]--] = b[i];
}
void suffix() {
for (int i = 1; i <= n; i++) //字母大小为第一关键字,位置为第二关键字
rk[i] = s[i], tp[i] = i;
rsort(rk, tp);
for (int l = 1, num = 0; num != n; l <<= 1) {
num = 0;
for (int i = n - l + 1; i <= n; i++) //在n-l+1后面的位置都无法找到l位后的后续,所以为0
tp[++num] = i; //所以先将位置存入
for (int i = 1; i <= n; i++) //sa[i]=排名i的后缀位置,如果>l,那么它就可以和sa[i]-l位置的第一关键字匹配
if (sa[i] > l)
tp[++num] = sa[i] - l;
//此刻tp数组内情况:[第二关键字为0的一组的起始位置......第二关键字按照从小到大的一组的起始位置]
rsort(rk, tp);
//此处分割,上述为k阶段的sa以及排序,下述为k<<1阶段的rk
swap(rk, tp); //用tp存储上一轮的rk,来为下一轮的rk做准备
rk[sa[num = 1]] = 1;
for (int i = 2; i <= n; i++) //排名相近的第一关键字和第二关键字是否相等
rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + l] == tp[sa[i - 1] + l]) ? num : ++num;
m = num;
}
return;
}
void get_height() {
for (int i = 1; i <= n; i++) //这句话其实可加可不加
rk[sa[i]] = i;
for (int i = 1, k = 0; i <= n; i++) {
if (k)
k--;
int j = sa[rk[i] - 1];
while (s[i + k] == s[j + k])
k++;
hg[rk[i]] = k;
}
}
public:
string longestDupSubstring(string S) {
s=" "+S;
n = s.length();
m = 122;
suffix();
get_height();
int idx=1,ans=0;
for (int i = 1; i <= n;i++)
if(hg[i]>ans){
ans=hg[i];
idx=sa[i];
}
return S.substr(idx-1,ans);
}
};
2021年4月4日
781. 森林中的兔子
兔兔能有什么坏心思呢?
兔兔要是有坏心思就吃掉好了。
题目要求输出最少个数,所以我们可以考虑,对于个数相同的并为一组,以减少增加。
例如,[1,1,2],那么我们将里面的[1,1]并为相同的颜色,那么这组最大就只有5个了。
那么,对于相同个数的,我们最少可以分成多少组呢?
设x是数字相同的个数,y是数字
那么,显然,相同数字并成一组就是y+1个
那么,分组数就是\(\lceil\frac{x}{y+1}\rceil\)
答案也就很显然了。
class Solution {
public:
int numRabbits(vector<int>& answers) {
unordered_map<int,int>cnt;
for(auto it:answers)
cnt[it]++;
int ans=0;
for(auto [y,x]:cnt)
// ans+=(x+y)/(y+1)*(y+1);
ans+=(y+1)*(x/(y+1)+(x%(y+1)!=0));
return ans;
}
};
2023年11月27日
907. 子数组的最小值之和
使用dp解决这个问题
-
首先考虑最简单的方式
根据题目的要求,我们可以知道:
\[假设:a[i][j]=min(a[i]...a[j]) \]我们让\(dp\)数组维护前\(i\)项最小值的和
\[\begin{align} dp[i]&=\sum_{k=0}^i\sum_{j=0}^ka[j][k]\\ & =\sum_{j=0}^i{a[j][i]}+dp[i-1] \end{align} \]当然,\(O(n^2)\)这样子做肯定超时。
-
考虑找一下规律
我们假设在\((i+1,j)\)区间,只有\(arr[i]<arr[j]\),这意味着\(arr[j]\)是这个区间内的最小值
那么对于\(\sum_{k=i+1}^j{a[k][j]}\)
我们可以得出一个很简单的结论:\(\sum_{k=i+1}^j{a[k][j]}=(j-i)*arr[j]\)
那么我们重新思考上面的dp公式
\[\begin{align} dp[i] & = \sum_{j=0}^i{a[j][i]}+dp[i-1]\\ &=\sum_{j=k+1}^i{a[j][i]}+dp[k]\\ &=(i-k)*arr[i]+dp[k] \end{align} \]那么如何寻找\(arr[i]\)左侧第一个小于等于它的呢?维护一个递增的单调栈即可。
代码
class Solution {
public int sumSubarrayMins(int[] arr) {
int len = arr.length;
int mod = (int) 1e9 + 7;
int dp[] = new int[len];
int stack[] = new int[len];
int ans = 0, cur = -1;
for (int i = 0; i < len; i++) {
while (cur != -1 && arr[i] < arr[stack[cur]]) {
cur--;
}
if (cur == -1) {
dp[i] = (i + 1) * arr[i] % mod;
} else {
dp[i] = dp[stack[cur]] + (i - stack[cur]) * arr[i] % mod;
}
cur++;
stack[cur] = i;
ans = (ans + dp[i]) % mod;
}
return ans;
}
}
2023年11月28日
1670. 设计前中后队列
思路与解析
又不是比赛,不追求解题的效率,所以就想着直接模拟做了。
三个指针,分别指向队首、队尾和中间。每次插入都对长度判断,根据奇偶来移动中间的指针。
然后就是debug、debug和debug
代码
懒得优化了,就这样吧……
class FrontMiddleBackQueue {
class Node {
Integer val;
Node next;
Node pre;
Node(int val) {
this.next = null;
this.pre = null;
this.val = val;
}
}
Node head, back, middle;
int len = 0;
public FrontMiddleBackQueue() {
head = new Node(-1);
back = new Node(-1);
middle = new Node(-1);
head.next = back;
back.pre = head;
middle = back;
}
public void pushFront(int val) {
Node node = new Node(val);
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
if (len == 0) {
middle = head.next;
} else if (len % 2 != 0) {
middle = middle.pre;
}
len++;
}
public void pushMiddle(int val) {
Node node = new Node(val);
if (len == 0) {
head.next = node;
back.pre = node;
node.pre = head;
node.next = back;
middle = node;
} else if (len % 2 != 0) {
node.next = middle;
node.pre = middle.pre;
middle.pre.next = node;
middle.pre = node;
} else {
node.next = middle.next;
node.pre = middle;
middle.next.pre = node;
middle.next = node;
}
middle = node;
len++;
}
public void pushBack(int val) {
Node node = new Node(val);
node.next = back;
node.pre = back.pre;
back.pre.next = node;
back.pre = node;
if (len == 0) {
middle = back.pre;
} else if (len % 2 == 0) {
middle = middle.next;
}
len++;
}
public int popFront() {
if (len == 0) {
return -1;
}
len--;
if (len == 0 || len % 2 != 0) {
middle = middle.next;
}
Node node = head.next;
int val = node.val;
node.next.pre = head;
head.next = node.next;
return val;
}
public int popMiddle() {
if (len == 0) {
return -1;
}
int val = middle.val;
len--;
middle.next.pre = middle.pre;
middle.pre.next = middle.next;
if (len == 0 || len % 2 != 0) {
middle = middle.next;
} else {
middle = middle.pre;
}
return val;
}
public int popBack() {
if (len == 0) {
return -1;
}
len--;
if (len == 0) {
middle = middle.next;
}
if (len % 2 == 0) {
middle = middle.pre;
}
Node node = back.pre;
int val = node.val;
node.pre.next = back;
back.pre = node.pre;
return val;
}
}
2023年11月29日
2336. 无限集中的最小数字
水题,\(Set\)直接做可以秒。当然想要快的做法,请往下看。
思路
参考桶排序的思路。
看一下数据范围\(1000\),那么定义一个数组new int[1001]
,只要\(pop\)就将arr[i]=1
。然后将minn++
,再从\(minn\)开始往后找,找到第一个桶不为\(1\)的就可以了。
\(add\)就把arr[i]=0
,如果i<minn
就把\(minn\)更新为\(i\)。
代码
class SmallestInfiniteSet {
int arr[];
int minn;
public SmallestInfiniteSet() {
arr = new int[1001];
minn = 1;
}
public int popSmallest() {
int val = minn;
arr[val] = 1;
minn++;
for (int i = minn; i < 1001; i++) {
if (arr[i] == 0) {
minn = i;
break;
}
}
return val;
}
public void addBack(int num) {
arr[num] = 0;
minn = minn > num ? num : minn;
}
}
2023年11月30日
1657. 确定两个字符串是否接近
思路
规律题
首先是word1
和word2
中出现的字符是同一集合的,即\(S_1=S_2\)。
其次是频次相同,即统计每个字符后,从小到大排序,word1
和word2
的排序结果是一样的。
代码
class Solution {
public boolean closeStrings(String word1, String word2) {
if (word1.length() != word2.length()) {
return false;
}
int[] count1 = new int[26];
int[] count2 = new int[26];
for (int i = 0; i < word1.length(); i++) {
count1[word1.charAt(i) - 'a']++;
count2[word2.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if ((count2[i] > 0) != (count1[i] > 0)) {
return false;
}
}
Arrays.sort(count1);
Arrays.sort(count2);
for (int i = 0; i < 26; i++) {
if (count1[i] != count2[i]) {
return false;
}
}
return true;
}
}