本专栏主要为了巩固基础算法,会把比赛中觉得比较好的题目拿过来整理一下。
本周主要是前缀和和查分专题,有很多经典的题目。另外还有本周的小白赛和atcoder。
一、前缀和差分专题
1.1 P1714 切蛋糕 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:我们直接看到形式化的部分:找到一个子段 [l,r] (r−l+1≤m)使得小z的得分最大。数据范围是5e5 要设计一个优于n^2的算法,很容易想到前缀和数组来优化区间遍历。假如本题的蛋糕得分为正整数或者只能吃m块(而不是最多)就是一个简单的模板题。
也就是说我们有些时候的最大的分并不是吃m块,要从左端点找出一个长度小于等于的右端点使得这部分子段最大,就要找出长度为m的窗口内的最大值,没错这题就是前缀和加单调队列的题。
🖥️code:
#include <iostream>
#include <deque>
using namespace std;
typedef long long LL;
const int N = 5e5 + 10;
LL f[N];
int n, m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> f[i];
f[i] += f[i - 1];
}
deque<int> q;
LL ans = -0x3f3f3f3f3f3f3f3f;
q.push_back(0);
for(int i = 1; i <= n; i++)
{
ans = max(ans, f[i] - f[q.front()]);
while(q.size() && f[i] <= f[q.back()]) q.pop_back();
q.push_back(i);
if(q.back() - q.front() + 1 > m) q.pop_front();
}
cout << ans << endl;
return 0;
}
1.2 P2629 好消息,坏消息 - 洛谷
题目来源:洛谷
题目难度:★
说点废话:这是一道来自洛谷的绿题,但是讲真的我很少有一眼看出绿题在考什么的时候,建议降黄。
【解题】:看了上面那一题我相信你一定对这个题很熟悉,假如我们只需要确认他从1 - n这个顺序开始报告会不会被炒问题就很简单:维护前缀和数组,找到其中的最小值,看是否会小于0即可。
但问题是他要在任意时刻开始呈现一个环形结构的报告(对于环形结构我们一般的做法是拷贝一份原数组或者取模操作),本题我们采用拷贝做法 ,原因是:他从1 - n 任意的位置k开始报告,只需确认k - k + n这个长度为n的前缀和区间内的最小值减去开始前一个位置的前缀和是否会出现小于0的情况即可,没错确定长度为n的窗口内的最小值还是需要单调队列。
🖥️code:
#include <iostream>
#include <deque>
using namespace std;
const int N = 2e6 + 10;
typedef long long LL;
LL a[N];
LL f[N];
int ret[N];
int n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i + n] = a[i];
}
for(int i = 1; i <= 2 * n; i++) f[i] = f[i - 1] + a[i];
int k = n;
deque<int> q;
for(int i = 1; i <= 2 * n; i++)
{
while(q.size() && f[i] <= f[q.back()]) q.pop_back();
q.push_back(i);
if(q.back() - q.front() + 1 > k) q.pop_front();
if(i >= k) ret[i] = q.front();
}
int ans = 0;
for(int i = 1; i <= n; i++)
{
if(f[ret[i + n - 1]] - f[i - 1] >= 0) ans++;
}
cout << ans << endl;
return 0;
}
1.3 P10904 [蓝桥杯 2024 省 C] 挖矿 - 洛谷
题目来源:洛谷
题目难度:★
【解题】:本题需要维护一个长度为m的区间(其实是路程为m的区间),使得挖的矿最多,将区间转化为端点问题需要用到前缀和,但问题是他会往回走,如何枚举所有的合法区间呢? 很简单我们只需枚举他往左走的步数k此时往右走的步数为m - 2 * k,还需要枚举往右走的步数原因是:在刚刚的枚举策略中,是先往左走再回来,重复的路段是左侧而不是右侧。
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e6 + 10;
LL forw[N], reve[N];
int n, m;
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i++)
{
int x; cin >> x;
if (x > 0) forw[x]++;
else if (x < 0) reve[-x]++;
else cnt++;
}
for (int i = 1; i <= N; i++)
{
forw[i] += forw[i - 1];
reve[i] += reve[i - 1];
}
LL ans = 0;
for (int i = 1; i <= m; i++)
{
// 正向走i步
LL t = 0;
if (m - 2 * i <= 0) continue;
t = cnt + forw[i] + reve[m - 2 * i];
ans = max(ans, t);
// 反向走i步
t = cnt + reve[i] + forw[m - 2 * i];
ans = max(ans, t);
}
cout << ans << endl;
return 0;
}
1. 4 AT_abc338_d [ABC338D] Island Tour - 洛谷
题目来源:AtCoder
题目难度:★★
本题是来自AtCoder的一个题,可以点标题下面的链接直接跳过去。
说点废话:说实话本题确实不好想,再加上我当时感冒连题解都没看明白QAQ。
【解题】: 差分和前缀和用于把区间问题化为端点为题。既然知道这题考试就是差分和前缀和,从某种意义上来说我们要找到对于连续一段区间的影响,那么我们来想想这个区间到底代表什么。
假如本题并不会切断某一处桥,从A -> B只有两种走法:顺时针走和逆时针走,要求步数最小那么根据贪心思想我们只需要选择两种走法的最小值即可。
下面讨论有桥的情况:
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n, m;
int a[N];
LL f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> a[i];
LL ans = 0;
// 1 -> n - 1
// 差分
for (int i = 1; i < m; i++)
{
int l = a[i], r = a[i + 1];
if (l > r) swap(l, r);
int len1 = r - l, len2 = n - (r - l);
ans += min(len1, len2);
if (len1 < len2) // 说明len1更优,
{
int t = len2 - len1;
// l - r
f[l] += t; f[r] -= t;
}
else // len2更优,
{
int t = len1 - len2;
// r - n 1 - l
f[r] += t; f[n + 1] -= t;
f[1] += t; f[l] -= t;
}
}
// 前缀和
for (int i = 1; i <= n; i++) f[i] += f[i - 1];
LL ret = 0x3f3f33f3f3f;
for (int i = 1; i <= n; i++) ret = min(ret, f[i]);
cout << ans + ret << endl;
return 0;
}
1.5 AT_abc353_d [ABC353D] Another Sigma Problem - 洛谷
题目来源:AtCoder
题目难度:★★
【解题】:本题的关键是对于表达式的理解:f(A1, A2) + f(A1, A3) + f(A1, A4) + f(A1, A5) + f(A1, A6) +...+ f(A1, An) + f(A2, A3) + f(A2, A4) + ... + f(A2, An) +...+ f(An - 1, An)。
上述公式展开式中我们发现:对于每个数当它作为底数时,前面的数的和与它拼接(前面数的和* 10^本数的位数)再加上本数出现的次数(其实就是前面数的个数)就是这个数对结果的贡献,将每个数的贡献累加就可以得到结果(不要忘记取模)。
在这里我们需要维护的数据:
1. 前缀和数组(每次都要取模防溢出)。
2. 10的阶乘取模数组(也是预处理的一部分,目的是避免重复的pow运算)
3. 每个数的位数(便于拼接操作)。
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10, MOD = 998244353;
LL A[N], a_mod[N];
int digts[N];
LL pow10_mod[19];
int n;
int get_digts(LL x)
{
if(x == 0) return 1;
int cnt = 0;
while(x)
{
cnt++;
x /= 10;
}
return cnt;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> A[i];
}
for(int i = 1; i <= n; i++)
{
LL x = A[i];
digts[i] = get_digts(x);
a_mod[i] = A[i] % MOD;
}
pow10_mod[1] = 10;
for(int i = 2; i <= 18; i++)
{
pow10_mod[i] = 1;
pow10_mod[i] = (pow10_mod[i - 1] * 10) % MOD;
// cout << pow10_mod[i] << " ";
}
LL ans = 0;
LL prev_sum = 0;
for(int i = 1; i <= n; i++)
{
LL part1 = prev_sum * pow10_mod[digts[i]] % MOD;
LL part2 = a_mod[i] * (i - 1) % MOD;
ans = (ans + part1 + part2) % MOD;
prev_sum += a_mod[i]; prev_sum %= MOD;
}
cout << ans << endl;
return 0;
}
二、 牛客小白月赛112
评价:ABCD是签到题,当然我也只做了也几道题。D题是图论的题,当然没学过其实也是一道简单的思维题。EF是动态规划问题,看出来考什么不难,但想要做出来玄学的DP也不容易。
2.1 智乃的“K”叉树
题目来源:牛客网
题目难度:★
【解题】:我们先把树当做一个无向无环图处理,题目中要我们选择一个根结点使得最大的k最小(我没用二分做,不知道行不行),发现当选择一个结点作为根时,除了本结点外,其余所有的结点的度都会转变成一个入度和剩下的出度,出度就是孩子结点的个数。
所以本题想要k最小,就要让度数最大的结点作为孩子结点,最小的k就是最大度数减一。
根结点的选择:第一个度数小于小于最大度数的结点。
🖥️code:
#include <iostream>
#include <vector> // vector建图
using namespace std;
const int N = 2e5 + 10;
vector<int> edges[N];
int n;
int main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
edges[x].push_back(y);
edges[y].push_back(x);
}
int ans = 0;
for(int i = 1; i <= n; i++)
{
int t = edges[i].size();
ans = max(ans, t);
}
cout << ans - 1 << " ";
for(int i = 1; i <= n; i++)
{
if(edges[i].size() < ans)
{
cout << i << endl;
break;
}
}
return 0;
}
三、AtCoder Beginner Contest 398
评价:AtCoder的题目是纯英文,赛制是ACM赛制,题目难度和牛客周赛差不多,难度呈递增走向。ABCD是签到题,我也只做了签到题,其中D题还是问的学长。本题补题BDF三题。
3.1 B - Full House 3
说点废话:为什么补这一道,是因为这一题我罚了三次时o(╥﹏╥)o;
【解题】:我的想法是用hash数组存,hashkey就是数字本身(因为数据范围很小),然后并行去找是否存在大于等于3和2的数并且这两个数是否相同,然而样例:1 1 2 2 2 2 3过不了。
正确解法:纯暴力,当出现大于等于三的数时再去找大于等于二的。
🖥️code:
#include <iostream>
using namespace std;
int count[14];
int main()
{
for (int i = 0; i < 7; ++i)
{
int a;
cin >> a;
count[a]++;
}
bool found = false;
for (int x = 1; x <= 13; ++x)
{
if (count[x] >= 3)
{
for (int y = 1; y <= 13; ++y)
{
if (x != y && count[y] >= 2)
{
found = true;
break;
}
}
if (found) break;
}
}
if(found) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
3.2 D - Bonfire
题目来源:AtCoder
题目难度:★★
、
【解题】:本题有点考物理的相对运动,当我们正面想问题时,需要记录每个烟的走向,然后还会产生新的烟,这样就迫使我们的时间复杂度一步步递增,而且这个代码想当然也不会好写,后来我又想只记录人周围的烟雾,但是这样对于烟雾的更新并无太大的帮助。
正确解法:正难则反,利用物理的相对运动,将烟雾看做静止,然后每次刮风时将人和篝火往反方向移动,记录篝火出现过得位置,人一旦走到就输出1否则输出0.
问题一:怎么存下这个出现的位置,利用二维数组并不现实,我们只需要快速检索i j这个pair是否出现过就可以,所以我们用双关键字的map来存(不要用unordered_map存,因为并没有对于pair的hashkey),第一个关键字存位置,第二个村是否出现过。
🖥️code:
#include <iostream>
#include <map>
using namespace std;
typedef pair<int, int> PII;
map<PII, int> mp;
int n, R, C;
int main()
{
cin >> n >> R >> C;
string s; cin >> s;
s = " " + s;
mp[{0, 0}]++;
int r = 0, c = 0;
for(int i = 1; i <= n; i++)
{
if(s[i] == 'N')
{
R++;
r++;
}
else if(s[i] == 'E')
{
C--;
c--;
}
else if(s[i] == 'W')
{
C++;
c++;
}
else
{
R--;
r--;
}
mp[{r, c}]++;
if(mp[{R, C}] != 0) cout << 1;
else cout << 0;
}
return 0;
}