【刷题节】美团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 解题思路

语法基础: 顺序结构 时间复杂度:

遍历一遍,找出字符串中 MT 的字符数量 ,答案即为 的最小值。

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;
}
#美团##算法##春招#
全部评论
点赞 回复 分享
发布于 02-28 09:58 湖北
m
点赞 回复 分享
发布于 02-28 01:17 上海

相关推荐

评论
5
9
分享

创作者周榜

更多
牛客网
牛客企业服务