赞
踩
背景一
目前,我所搜索到的全网博文关于小美的朋友关系
都没有通过的代码。思路是正确的,但是代码都是超时或答案错误。鉴于目前官方也没有发布题解,遂写此篇博客供大家交流学习
背景二
中午午休的时候,科科公主给我甩来一个链接,让我陪她一起做题。做了 80 80 80 分钟左右,把除了小美朋友关系
其他题目写完,就去上课了。后面吃完晚饭,又补了小美朋友关系
这道题。
整体评价
美团的算法笔试,没有我想象的那么难。
我之前看大厂招算法岗,都需要研究生学历。所以期望值很高,做了一下发现不是太难。大概就等同于中学OI的普及组难度。
题目链接:小美的平衡矩阵 —— 牛客网
算法思想:
前缀和+遍历
时间复杂度:
O
(
n
3
)
O(n^3)
O(n3)
n n n 的数据量只有 200 200 200,依次遍历矩形边长 k ∈ [ 1 , n ] k \in [1, n] k∈[1,n]:
#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; } }
题目链接:小美的数组询问 —— 牛客网
语法基础:
循环结构
时间复杂度:
O
(
n
+
q
)
O(n+q)
O(n+q)
#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; } }
题目链接:小美的 MT —— 牛客网
语法基础:
顺序结构
时间复杂度:
O
(
n
)
O(n)
O(n)
遍历一遍,找出字符串中 M
和 T
的字符数量
c
n
t
cnt
cnt,答案即为
c
n
t
+
k
cnt + k
cnt+k 和
n
n
n 的最小值。
#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);
}
题目链接:小美的朋友关系 —— 牛客网
算法思想:
逆向构建并查集
时间复杂度:
O
(
m
log
m
)
O(m\log m)
O(mlogm)
该题与一个并查集的经典题目 P1197 [JSOI2008] 星球大战 相似,都是用逆向思维维护并查集。
删除结点关系并不好处理,但是如果我们逆向进行 q q q 次操作,先构建出操作之后最终的关系网,然后倒着重新进行操作。那么此时删除关系,就变为了添加关系。而并查集就是处理关系的一把好手。
易错点提示:
map
存储;init()
操作一样,从
1
1
1 到
1
e
9
1e9
1e9 循环
f
[
i
]
=
i
f[i] = i
f[i]=i。只有当出现的结点,我们才需要
f
[
i
]
=
i
f[i] = i
f[i]=i,所以不用全部赋值,会超时!#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; }
题目链接:小美的区间删除 —— 牛客网
算法思想:
双指针、前缀和、容斥定理、一点点数论小知识
时间复杂度:
最坏时间复杂度:
O
(
w
×
n
)
,
w
=
log
(
1
e
9
)
O(w\times n),w=\log(1e9)
O(w×n),w=log(1e9)
5. 1. 1 乘积末尾 0 0 0 的个数问题分析:
5.1.2 删除区间的计数问题分析:
所以现在问题转化为求解可删除的最长区间。
以数组 10 , 3 , 3 , 10 10, 3,3,10 10,3,3,10 为例,保证剩余元素乘积至少有一个 0 0 0。
我们可以利用双指针,固定区间左边界
l
l
l,不断移动区间右边界
r
r
r,利用5.1.1 方法判断末尾
0
0
0 的个数,直至使得删除
[
l
,
r
]
[l,r]
[l,r] 后,末尾
0
0
0 的个数小于
k
k
k,
r
r
r 停止右移动。那么此时,
[
l
,
r
−
1
]
[l,r-1]
[l,r−1] 就是以
l
l
l 为左边界,最长的连续区间范围。
按上述例子,即
l
=
1
,
r
=
4
l = 1, r = 4
l=1,r=4时停止移动,此时最大删除区间为
[
1
,
3
]
[1, 3]
[1,3]。
统计完一个区间之后,要移动
l
l
l 的值,找到新的可以删除区间。
也就是右移动
l
l
l 直到剩余区间的
0
0
0 的个数再次不小于
k
k
k时停止;或者
l
l
l 与
r
r
r 重合时停止。此时就可以再次固定
l
l
l 的位置,不断探测找到最大
r
r
r 值。
按上述例子,
l
l
l 移动到下标
2
2
2 时,剩余元素乘积末尾的
0
0
0 再次不小于
1
1
1。然后以
l
=
2
l = 2
l=2 再次探测最长区间。即
l
=
2
,
r
=
5
l = 2, r = 5
l=2,r=5时停止移动,此时最大删除区间为
[
2
,
4
]
[2, 4]
[2,4]。
根据上述的例子,我们不难看出两次最长区间 [ 1 , 3 ] , [ 2 , 4 ] [1,3] , [2,4] [1,3],[2,4] 中有一段重叠区间。这个区间在第一次时已经被统计进删除方案了,第二次又被统计了一次。根据容斥定理,我们要把这一段重复的方案数再减掉。
至此,这个题目的解题思路就结束了。
#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; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。