尝试打打写写题解。
只写了的,后两题一点思路没有也就没补了()。
题目链接:https://atcoder.jp/contests/abc457/tasks
A - Array
按照题目要求写即可。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define endl '\n'#define mod 998244353typedef pair<int, int> pii;
int a[110];
void solved(){ int n, x; cin >> n; for(int i = 1; i <= n;i++){ cin >> a[i]; } cin >> x; cout << a[x] << endl;}
int main(){ ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr); int t = 1; while(t--){ solved(); } return 0;}B - Arrays
这里其实是要输入一个每行元素数量不同的特殊二维数组,然后输出对应索引的元素,我这里用了。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define endl '\n'#define mod 998244353typedef pair<int, int> pii;
map<int, vector<int>> mp;
void solved(){ int n; cin >> n; for(int i = 1; i <= n;i++){ int t; cin >> t; while(t--){ int x; cin >> x; mp[i].push_back(x); } } int x, y; cin >> x >> y; cout << mp[x][y - 1] << endl;}
int main(){ ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr); int t = 1; while(t--){ solved(); } return 0;}C - Long Sequence
这里构造的二维数组跟上一题一样。然后要我们构造一个由每个序列的元素按个轮次反复压入数组而成数组。我们按它的要求真的压入就会超时。所以我们的选择是判断落在哪个序列压入的范围。让(前面压入的总范围),再对这个序列的元素个数取模,求出具体在单个序列中这个的索引是多少。然后输出这个索引对应的元素即可。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define endl '\n'#define mod 998244353typedef pair<int, int> pii;
map<int, vector<ll>> mp;ll c[200010];
void solved(){ ll n, k; cin >> n >> k; for(int i = 1; i <= n;i++){ int t; cin >> t; while(t--){ int x; cin >> x; mp[i].push_back(x); } } for(int i = 1; i <= n;i++){ cin >> c[i]; } for(int i = 1; i <= n;i++){ ll size = mp[i].size(); if(k > size * c[i]){ k -= size * c[i]; } else if(k == size * c[i]){ cout << mp[i][size - 1] << endl; break; } else{ ll temp = k % size; if(temp == 0){ k = size - 1;} else {k = temp - 1;} cout << mp[i][k] << endl; break; } }}
int main(){ ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr); int _ = 1; while(_--){ solved(); } return 0;}D - Raise Minimum
这题我们需要转变一下思路。我们如果想要去模拟这个操作的过程肯定是会超时的。但是我们完全可以不去模拟,而是尝试去猜答案,看这个答案是否能够符合条件,然后不断找到一个能满足条件的最大值。而这其实就是二分法。
我们这里用的贪心二分答案。这里的检查逻辑是如果小于目前猜的最小值就进行操作计数。我这里的判断逻辑不是的倍数的就取商加,是就直接取商。(其实还可以写简便一点)然后比较需要的操作次数是否小于等于最大的操作次数限制,是就继续往大的值猜,否就往小的猜。
这里的二分法右端最大值应该取。我们考虑一个极端情况。对于,假设它和最大值相同,然后全部操作都用在它上面,那么最大值也只能去到。而这就是序列最小值的能达到的极限最大值了。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define endl '\n'#define mod 998244353typedef pair<int, int> pii;
ll a[200010];ll n, k;ll mina = 1e18 + 10, maxa = 0;
bool check(ll x){ ll cnt = 0; for(int i = 1; i <= n;i++){ if(a[i] < x){ if((x - a[i]) % i != 0){ cnt += ((x - a[i]) / i + 1); } else{ cnt += ((x - a[i]) / i); } if(cnt > k) return false; } } return cnt <= k;}
void solved(){ cin >> n >> k; for (int i = 1; i <= n;i++){ cin >> a[i]; if(a[i] > maxa) {maxa = a[i];} if(a[i] < mina) {mina = a[i];} } ll l = mina, r = maxa + k; ll ans = mina; while(l <= r){ ll mid = l + (r - l) / 2; if(check(mid)){ ans = mid; l = mid + 1; } else{ r = mid - 1; } } cout << ans << endl;}
int main(){ ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr); int _ = 1; while(_--){ solved(); } return 0;}E - Crossing Table Cloth
这题依旧是二分题。但是要复杂很多。
核心思路是对于每一次查询时的左(右)端点,我们用一个数组存入它所有右(左)端点的下标。然后通过二分查找找到第一个满足条件的右(左)端点。也就是进行两次查找。(一次以左端点为准,找满足条件的右端点,另一次以右端点为准,找满足条件的左端点)然后通过条件判断。
找不到满足条件的右(左)端点。
两个区间断开了。
满足条件的只有一个区间(这个区间的左端点就是,右端点就是,且没有完全包含在内的子区间)。
这三种情况就直接掉,如果满足就是了。
这里的前两个条件都很好判断,详细都写在注释里了。
但是这里的判断是否有子区间就比较困难。如果正常地循环去找子区间就是会直接超时,这里我们引入了树状数组来优化这种区间查询的情况(可以实现)。最终我们时间复杂度可以压到可以通过题目。
#include <bits/stdc++.h>
using namespace std;using ll = long long;#define endl '\n'#define mod 998244353typedef pair<int, int> pii;
int n, m;
// 树状数组struct BIT { int n; vector<int> bit; BIT(int n_) : n(n_), bit(n_ + 2, 0) {} //初始化结构体 void add(int i, int v) { for(; i <= n; i += i & -i) bit[i] += v; //先给当前的区间加值,然后不断给父区间(包含这个区间的大区间)也加值 } int sum(int i){ int s = 0; for (; i > 0; i -= i & -i) s += bit[i]; //从最大区间开始,不断查找小区间,累加小区间的值计入总和,然后返回这个总和 return s; }};
void solved(){ cin >> n >> m; vector<vector<int>> L(n + 1), R(n + 1); //对应每一个左(右)端点,向数组中存入对应右(左)端点的下标
for(int i = 0; i < m;i++){ int l, r; cin >> l >> r; L[l].push_back(r); R[r].push_back(l); }
//保证二分的单调性 for(int i = 1; i <= n;i++){ sort(L[i].begin(), L[i].end()); sort(R[i].begin(), R[i].end()); }
//查询 int q; cin >> q; vector<array<int, 3>> Q(q); //对应的每次查询i,Q[i][0]表示左端点,Q[i][0]右端点,Q[i][2]为此时对应的下标i for (int i = 0; i < q; i++){ cin >> Q[i][0] >> Q[i][1]; Q[i][2] = i; //保存下标的原因是因为后续要用树状数组需要重新排序,但对应结果要存入正确的下标。 }
vector<array<int, 3>> sorted_Q = Q; //复制一个排序的Q方便树状数组操作 sort(sorted_Q.begin(), sorted_Q.end(), [](auto& a, auto& b){ return a[0] > b[0]; //对Q[i][0]从大到小排序,后续我们记录右端点要从大到小进行。 });
BIT bit(n); //创建树状数组 int cur = n; //cur记录当前操作到的下标。 vector<int> cnt_within(q, 0); //记录每次查询时,对应给出的区间内含有多少个子区间(包含它给出的这个区间本身)
for(auto & [s, t, idx] : sorted_Q){ while(cur >= s){ //当目前操作到的cur比目前的s大的话就继续向L[cur]里存有的右端点记入树状数组中,因为此时左端点>=cur的值都已经被加入BIT里了。 for (int r : L[cur]){ bit.add(r, 1); //往r下标在树状数组中的区间加1(也就是bit[r]的值加1),然后会在BIT里面继续也给父区间加1。 } cur--; //当前cur的右端点都计入了之后就下一个cur } cnt_within[idx] = bit.sum(t); //对于一个已经完成的查询内的s(所有右端点已经计入),我们就计算它和它的子区间的总个数和(也就是我们存入的值) }
//针对原来正常顺序的每一次查询进行二分 for(auto & [s, t, idx] : Q){ auto& vecL = L[s]; auto itR = upper_bound(vecL.begin(), vecL.end(), t); //找到第一个大于t的右端点itR,后续会itR--(也就是对应小于或者等于t的那个下标) if(itR == vecL.begin()) { //说明第一个元素就比t大,没有符合条件的itR,直接no掉 cout << "No" << endl; continue; } itR--; int maxR = *itR; //存入此时itR对应的值(也就是满足条件的最大的右端点t的下标)
auto& vecR = R[t]; auto itL = lower_bound(vecR.begin(), vecR.end(), s); //找到第一个大于等于左端点s的itL,所以不用减。 if(itL == vecR.end()){ //如果最后一个元素都不能满足就直接no掉 cout << "No" << endl; continue; } int minL = *itL; //存入此时的itL的值(也就是满足条件的最小的左端点的s的下标)
//如果两个区间没有连接在一起就no掉 if(maxR < minL - 1) { cout << "No" << endl; continue; }
//判断我们找到的两个区间是否最左端点是S,最右端点是T,并且区间内有两个及以上的子区间(包含本身)。是则yes bool has_st = (maxR == t && minL == s); if(has_st && cnt_within[idx] == 1){ cout << "No" << endl; } else{ cout << "Yes" << endl; } }}
int main(){ ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr); solved(); return 0;}树状数组
我们拿这道题目用到的来说明一下。
我们首先要理解里的代表的其实是一个区间。比如其实代表的是这个区间,代表的是这个区间。它是这样理解的。对应的区间长度由它的二进制从右往左第一次出现的位置对应的的幂值来决定。的二进制为,所以它对应的区间长度就是,的是,所以它的区间长度是,同理就是。所以我们可以通过这种方式划分区间:
对应,对应,对应,对应,以此类推,,,,。
那么我们就会发现一个规律,每到一个二次幂就是一个大区间,这个大区间我们就可以存放对应区间含有的所有的值。所以我们在插入值时,我们就可以利用(本质上就是根据区间长度进行跳转)这个条件来不断跳转我们要赋值的对应的。然后我们发现一旦它跳转到了一个大区间(二次幂对应的区间),它接下来就只会在大区间里继续向上跳了。(因为大区间的区间长度都是二次幂,跳转也是这个大小)因此通过这种方式,我们小区间内的值都会被传递到大区间内然后不断向上传递,这里其实就是插入值的原理,显然是一个级别的插入。
同理我们来看看查询一个对应区间内的总值(也就是)应该怎么查询。我们查询一个右端点,也就是最大的所在的区间。我们通过向下跳转,会不断向下面的区间跳转。每跳转一个区间我们就将其中的值加入总和中。然后一旦跳转到一个大区间(二次幂对应的区间),下一步就会直接结束循环了,而此时大区间对应的值就是前面所有的总值了,(插入的原理)我们本次的查询就结束了。显然通过这种跳转的方式,查询也是一个级别的查询。
在本题中树状数组的优化就显得十分高效,利用方式也完全基于这个原理。只不过区间最小端点变成了查询时的,而不是。
NOTE这里的其实就是原数和补码取合,然后就会取到二进制里从右往左第一个为的位对应的二次幂。
// 树状数组struct BIT { int n; vector<int> bit; BIT(int n_) : n(n_), bit(n_ + 2, 0) {} //初始化结构体 void add(int i, int v) { for(; i <= n; i += i & -i) bit[i] += v; //先给当前的区间加值,然后不断给父区间(包含这个区间的大区间)也加值 } int sum(int i){ int s = 0; for (; i > 0; i -= i & -i) s += bit[i]; //从最大区间开始,不断查找小区间,累加小区间的值计入总和,然后返回这个总和 return s; }};