【刷题节】美团2024年春招第一场笔试【算法策略】题解
注:本篇文章于2024年3月17日,首次发布于CSDN: CSDN原博客
后又在牛客开通个人博客,遂将此篇题解移植到牛客讨论区。
@TOC
前言 —— 瞎扯淡
背景一 目前,我所搜索到的全网博文关于
小美的朋友关系
都没有通过的代码。思路是正确的,但是代码都是超时或答案错误。鉴于目前官方也没有发布题解,遂写此篇博客供大家交流学习
背景二 中午午休的时候,科科公主给我甩来一个链接,让我陪她一起做题。做了
分钟左右,把除了
小美朋友关系
其他题目写完,就去上课了。后面吃完晚饭,又补了小美朋友关系
这道题。
整体评价 美团的算法笔试,没有我想象的那么难。 我之前看大厂招算法岗,都需要研究生学历。所以期望值很高,做了一下发现不是太难。大概就等同于中学OI的普及组难度。
1. 小美的平衡矩阵
题目链接:小美的平衡矩阵 —— 牛客网
1.1 解题思路
算法思想:
前缀和+遍历
时间复杂度:
的数据量只有
,依次遍历矩形边长
:
- 对于每个矩形,已知边长
,只用每次遍历矩形的左上定点,就可以确定整个矩形范围。
- 然后统计该矩形中 01 的具体数量,判断是否相等。
而这一步可以使用前缀和,建立数组
,来统计矩形
到
中
的数量。
- 那么对于 左上顶点
的矩阵,右下顶点即为
,其中
。
- 则字符
的数量即为
,字符
的数量为
。
1.2 AC代码
#include <iostream>
using namespace std;
const int N = 200;
int a[N][N], s[N][N];// a为原数组,s为前缀和数组
int main() {
int n; cin >> n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++){
scanf("%1d", &a[i][j]);
s[i][j] = s[i-1][j] + s[i][j-1] + a[i][j] - s[i-1][j-1];
}
for(int k=1; k<=n; k++){
if(k&1){// 当边长为奇数时,字符 1的数量不可能等于字符 0的数量
cout << 0 << endl;
continue;
}
int ans = 0;
for(int i=1; i+k-1<=n; i++)
for(int j=1; j+k-1<=n; j++){
int x = i + k - 1;
int y = j + k - 1;
if(s[x][y] - s[i-1][y] - s[x][j-1] + s[i-1][j-1] == k*k/2) ans++;
}
cout << ans << endl;
}
}
2. 小美的数组询问
题目链接:小美的数组询问 —— 牛客网
2.1 解题思路
语法基础:
循环结构
时间复杂度:
- 统计数组中
的个数
,以及数组的累加总和
。
- 对于每一个
区间,最小值为
,最大值为
。
2.2 AC代码
#include <iostream>
using namespace std;
const int N = 1e5;
long long a[N+5];
int main() {
int n, q; cin >> n >> q;
long long sum = 0;
int cnt = 0;
for(int i=0; i<n; i++) {
cin >> a[i], sum += a[i];
if(!a[i]) cnt++;
}
while(q--){
int l, r; cin >> l >> r;
cout << sum + cnt * l <<" "<<sum + cnt * r << endl;
}
}
3. 小美的 MT
题目链接:小美的 MT —— 牛客网
3.1 解题思路
语法基础:
顺序结构
时间复杂度:
遍历一遍,找出字符串中 M
和 T
的字符数量 ,答案即为
和
的最小值。
3.2 AC代码
#include <iostream>
using namespace std;
int main() {
int n, q; cin >> n >> q;
string s; cin >> s;
int cnt = 0;
for(int i=0; i<s.length(); i++){
if(s[i] =='M' || s[i]=='T') cnt++;
}
cout << min(n, cnt+q);
}
4. 小美的朋友关系
题目链接:小美的朋友关系 —— 牛客网
4.1 解题思路
算法思想:
逆向构建并查集
时间复杂度:
该题与一个并查集的经典题目 P1197 [JSOI2008] 星球大战 相似,都是用逆向思维维护并查集。
删除结点关系并不好处理,但是如果我们逆向进行 次操作,先构建出操作之后最终的关系网,然后倒着重新进行操作。那么此时删除关系,就变为了添加关系。而并查集就是处理关系的一把好手。
- 所以,第一步得到最终的关系网。我们先用
集合,记录下结点之间的关系。然后,遍历一遍
次操作,当操作为删除关系的时候,我们就删去集合中的关系。我们根据删除后的结点建立一个并查集。
- 然后,我们倒着对
次操作进行遍历。当询问结点
的时候,我们就判断
和
的父节点是否相同。当遇到删除
操作的时候,我们就添加
的关系。
易错点提示:
- 结点范围为
,所以要父节点数组要用
map
存储; - 因为数据量很大,所以需要路径压缩,降低时限。所以需要初始化
。 但是,不能按照并查集模板中
init()
操作一样,从到
循环
。只有当出现的结点,我们才需要
,所以不用全部赋值,会超时!
- 当正着从关系集合中,删除
时,不能仅仅删除
,还要考虑关系集合中是以
的形式存储;
- 倒着遍历的时候,如果遇到删除结点
的操作,如果
原本就没有关系,是不可以添加的;所以不妨新建一个操作数组
,将查询操作和删除关系集合中存在结点的命令单独存储,把删除关系集合中不存在的这种干扰操作舍掉;
4.2 AC代码
#include <iostream>
#include <set>
#include <vector>
#include <map>
using namespace std;
const int N = 1e5;
using PII = pair<int,int>;
int n, m, q;
map<int,int>f;// 并查集父亲数组
struct node{
int op, u, v;
}ord[N+5];// 新的操作数组
int find(int x){// 路径压缩
while(f[x] != x) x = f[x] = f[f[x]];
return x;
}
void merge(int u,int v){// 并查集合并
int fa = find(u);
int fb = find(v);
f[fb] = fa;
}
int main() {
cin >> n >> m >> q;
set<PII>st;// 关系集合
for(int i=0, u, v; i<m; i++){
cin >> u >> v;
st.insert({u, v}); // u, v放进关系集合中
f[u] = u, f[v] = v;// 把出现的结点父节点设置为自己
}
int num = 0;// 新的操作数组长度
for(int i=0; i<q; i++){
int op, u, v; cin >> op >> u >> v;
//如果是查询操作,可以直接存入
// 如果是删除操作,需要判断原关系集合中是否存在
if(op == 1){
// 可能是 {u,v} 形式存储
if(st.find({u, v}) != st.end()) st.erase({u, v});
// 可能是 {v,u} 形式存储
else if(st.find({v, u}) != st.end()) st.erase({v, u});
// 如果不存在直接跳过,不储存此次删除操作
else continue;
}
// 存入新的操作数组中
ord[num++] = {op, u, v};
}
// 删除之后,剩余关系集合就是没有涉及到的,也是最终的并查集
for(auto [u,v]:st) merge(u, v);
vector<bool>ans;// 存储答案
for(int i=num-1; i>=0; i--){// 倒着重新进行操作
int op = ord[i].op, u = ord[i].u, v = ord[i].v;
if(op == 1) merge(u, v);// 如果是删除操作,反过来进行合并
else{
// 当 f[u] = 0时,就是第一次出现该节点,需要初始化f[i]=i,方便进行路径压缩
if(!f[u]) f[u] = u;
if(!f[v]) f[v] = v;
ans.emplace_back(find(u) == find(v));// 查询操作,就储存答案
}
}
//因为是倒着遍历操作的,所以答案是倒着存储的
for(int i=ans.size()-1; i>=0; i--)
if(ans[i]) cout << "Yes" << endl;
else cout << "No" << endl;
}
5. 小美的区间删除
题目链接:小美的区间删除 —— 牛客网
5.1 解题思路
算法思想:
双指针、前缀和、容斥定理、一点点数论小知识
时间复杂度:
最坏时间复杂度:
5. 1. 1 乘积末尾 的个数问题分析:
- 对于乘积会产生
的,本质上都是由
与
的乘积产生。如
,这个10的产生过程为
。所以要求末尾
的个数,只用知道乘数中有多少
和
的数量,乘积
的数量就是两者取最小值。
- 所以我们只需用前缀和预处理出数组中
和
的个数
。
- 当去掉区间
, 则剩下区间
。 那么
的个数即为两个区间内个数之和
,同理
的个数为
。
- 那么末尾
的个数就是两者中的最小值。
5.1.2 删除区间的计数问题分析:
- 首先我们假设已知 下标为
,区间长度
的区间 可以删除。 那么我们可以删除这个区间中任意连续子区间。 子区间长度为
,有
种方案;长度为
,有
方案;
,长度为
,有
种方案。即这个区间内总删除方案数有
。
- 有上面分析之后,我们统计所有删除方案,只需要找到可以删除的最长区间,然后利用上面公式,就可以求出该区间的所有删除方案。
所以现在问题转化为求解可删除的最长区间。
以数组
为例,保证剩余元素乘积至少有一个
。
-
我们可以利用双指针,固定区间左边界
,不断移动区间右边界
,利用5.1.1 方法判断末尾
的个数,直至使得删除
后,末尾
的个数小于
,
停止右移动。那么此时,
就是以
为左边界,最长的连续区间范围。 按上述例子,即
时停止移动,此时最大删除区间为
。
-
统计完一个区间之后,要移动
的值,找到新的可以删除区间。 也就是右移动
直到剩余区间的
的个数再次不小于
时停止;或者
与
重合时停止。此时就可以再次固定
的位置,不断探测找到最大
值。 按上述例子,
移动到下标
时,剩余元素乘积末尾的
再次不小于
。然后以
再次探测最长区间。即
时停止移动,此时最大删除区间为
。
-
根据上述的例子,我们不难看出两次最长区间
中有一段重叠区间。这个区间在第一次时已经被统计进删除方案了,第二次又被统计了一次。根据容斥定理,我们要把这一段重复的方案数再减掉。
至此,这个题目的解题思路就结束了。
5.2 AC代码
#include <iostream>
#include <cmath>
using namespace std;
const int N = 1e5;
int n, a[N+5];
// two为前i个数中2的个数,five为前i个数中5的个数
int two[N+5], five[N+5];
int get(int i, int j){
// 即2.1.1方案,求出去掉区间[i,j]后乘积末尾 0的个数
return min(two[n]-two[j]+two[i-1], five[i-1]+five[n]-five[j]);
}
int main() {
int k; cin >> n >> k;
for(int i=1; i<=n; i++) cin >> a[i];
for(int i=1; i<=n; i++){
// 求因子2,5的个数
while(a[i]%2==0) two[i]++, a[i]/=2;
while(a[i]%5==0) five[i]++, a[i]/=5;
// 维护前缀和
two[i] += two[i-1];
five[i] += five[i-1];
}
int l = 0, r = 0;// 记录上一次删除区间,防止重复相加
long long ans = 0;
for(int i=1, j=1; i<=n && j<=n; ){
j = max(j, i);// 当 i右移超过j后,j不能比 i小,所以需要更新一下
while(j <=n && get(i, j) >= k) j++;//当剩余区间值不小于k,就不断向右移
ans += 1LL*(j-i)*(j-i+1)/2;// 删除方案即该区间的等差数列公式
if(r >= i) ans -= 1LL*(r-i)*(r-i+1)/2;
//如果上一个区间[l,r]的右端点不小于本次区间[i,j]的左端点,则产生重复需要删去 [i, r]方案数
l = i, r = j;//上次删除区间更新为[i,j]
while(i <= j && get(i, j)<k) i++;//右移i,直到再次可以删除 或 左右边界重合 停止
}
cout << ans;
}
#美团##算法##春招#