2024.1.19训练赛总结
赛时做出 ABC
链接 https://vjudge.net/contest/604628
A题
思路:先处理要求相等的输入,将被要求相等的变量放在一个并查集中,然后对要求不相等的输入进行判断,如果要求 xi != xj,但 xi 和 xj 在同一个并查集中,则输出 NO,否则 YES
问题:
第一,要不是看了原题,我都不知道下标有1e11的范围,这个只需要简单离散化就可以
第二,数组又开小了,导致一直 wa 和 mle服了

#include<bits/stdc++.h> const int MAXN = 1e5 + 10; using namespace std; typedef long long LL; struct node{ LL a,b; int op; }; node q[MAXN], ne[MAXN]; int T; int n; int cnt, cnt2; LL tot[MAXN << 1]; int f[MAXN]; map<LL, int> mp; int findfa(int x) { if(x == f[x]) return f[x]; return findfa(f[x]); } int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>T; while(T--) { cin>>n; mp.clear(); cnt = 0; cnt2 = 0; for(int i = 1;i <= n;i++) cin>> q[i].a >> q[i].b >> q[i].op, tot[++cnt2] = q[i].a, tot[++cnt2] = q[i].b; sort(tot + 1, tot + cnt2 + 1); tot[cnt2 + 1] = -1; for(int i = 1;i <= cnt2;i++) if(tot[i] != tot[i+1]) mp[tot[i]] = ++cnt; for(int i = 1;i <= cnt;i++) f[i] = i; cnt = 0; int op, l, r; for(int i = 1;i <= n;i++) { op = q[i].op; l = mp[q[i].a]; r = mp[q[i].b]; if(op) { int f1 = findfa(l), f2 = findfa(r); f[max(f2, f1)] = min(f2, f1); } else ne[++cnt].a = l, ne[cnt].b = r; } int flag = 1; for(int i = 1;i <= cnt;i++) { if(findfa(ne[i].a) == findfa(ne[i].b)) { flag = 0; break; } } if(flag) cout<<"YES\n"; else cout<<"NO\n"; } return 0; }
B题
思路:直接暴力模拟就可以了,直接用一个优先队列来存储区间,每次从队列里面取出一个最靠左,最长的区间,取中间赋值,再把两边的区间放到队列里,注意什么时候要放进队列,什么时候不需要放进队列就可以。比如出现了左边界大于右边界,边界超出了1和n的范围等
问题:
1.优先队列是大根堆,一直不记
2.在结构体中只能自定义小于号,不能大于号(存疑)
3.没注意边界问题,又wa了

#include<bits/stdc++.h> const int MAXN = 2e5 + 10; using namespace std; int T; int n; int ans[MAXN]; struct node{ int l; int r; friend operator < (const node a,const node b){ if(a.r - b.r == a.l - b.l) return a.l > b.l; return a.r - a.l < b.r - b.l; } }; priority_queue <node> q; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>T; while(T--) { cin>>n; int cnt = 0; while(!q.empty()) q.pop(); q.push({1,n}); while(!q.empty()) { node ne = q.top(); q.pop(); if(!ans[ne.l + ne.r >> 1]) ans[ne.l + ne.r >> 1] = ++cnt; if(ne.l >= ne.r || !ne.l || !ne.r || ne.l > n || ne.r > n) continue; q.push({ne.l, (ne.l + ne.r >> 1) - 1}); q.push({(ne.l + ne.r >> 1) + 1, ne.r}); } for(int i = 1;i <= n;i++) cout<<ans[i]<<" \n"[i==n],ans[i] = 0; } return 0; }
C题
思路:一开始还没看出来是个经典线段树,后面才想明白。
每个节点除了保存边界,再多一个变量存储左右子树的 gcd 就可以了。
问题在于如何询问,即确定在所给区间内最多修改一个值,使得该区间的 gcd 恰好为 x。
做的时候的思路是:
求出来需要修改的次数,如果小于等于1,就 yes,否则 no
如果当前节点表示的区间是被包含在所给区间的,那就根据它的 gcd 值分以下几种情况来判断:
1.如果是 x 的倍数,说明不需要修改
2.如果不是 x 的倍数,如果是单个节点,修改次数+1,否则分别对它的两个子区间进行询问
关于为什么只要是 x 的倍数都不用修改,因为如果区间的 gcd 是 x 的倍数,那么只需要修改一次,随便把一个数修改成 x,最终就都满足了
而如果该区间有数不含 x 这个因子,或者两个或多个数的 gcd 不为 x,那就需要把它们全部修改成 x 了,所以有几个就得改几个,而且不需要修改那些 gcd 是 x 或 x 倍数的区间,因为一旦区间中有了 x,那整个区间的 gcd 一定不会超过 x。
问题:
如果不加优化,会有很明显的问题: 超时
这就需要剪枝了,只要当前需要修改的次数已经大于1,那就直接结束。(居然当时没想到)
总结:很好线段树,让我找回感觉,不再恐惧,第一次手写过。

#include<bits/stdc++.h> const int MAXN = 5e5 + 10; using namespace std; int n; int a[MAXN]; int q; int op, poi, ll, rr, xx; struct node{ int l; int r; int g; }t[MAXN<<3]; int gcd(int a,int b) { if(!b) return a; return gcd(b, a % b); } void build(int rt, int L, int R) { if(L == R) { t[rt].l = t[rt].r = L; t[rt].g = a[L]; return; } int mid = L + R >> 1; build(rt << 1, L, mid); build(rt << 1 | 1, mid + 1, R); t[rt].l = L; t[rt].r = R; t[rt].g = gcd(t[rt << 1].g, t[rt << 1 | 1].g); return; } void update(int rt, int p, int x) { if(t[rt].l == t[rt].r && t[rt].l == p) { t[rt].g = x; return ; } int mid = t[rt].l + t[rt].r >> 1; if(p <= mid) update(rt << 1, p, x); else update(rt << 1 | 1, p, x); t[rt].g = gcd(t[rt << 1].g, t[rt << 1 | 1].g); return; } int summ = 0; void query(int rt) { if(summ > 1) return ; // ¼ôÖ¦ ºÜÖØÒª if(ll > t[rt].r || rr < t[rt].l) return ; int mid = t[rt].r + t[rt].l >> 1; if(ll <= t[rt].l && rr >= t[rt].r) { if(t[rt].g % xx == 0) return ; else { if(t[rt].l == t[rt].r) { summ++; return ; } query(rt << 1); query(rt << 1 | 1); return ; } } if(mid >= ll) query(rt << 1); if(rr > mid) query(rt << 1 | 1); return; } int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(int i = 1;i <= n;i++) cin>> a[i]; build(1, 1, n); cin>>q; for(int i = 1;i <= q;i++) { cin>>op; if(op == 1) { cin>> ll >> rr >> xx; query(1); cout<<summ<<" "; if(summ <= 1) cout<<"YES\n"; else cout<<"NO\n"; summ = 0; } else { cin>> poi >> xx; update(1, poi, xx); } } return 0; }
总结一下写的前三题:
1.小失误太多了,不过这么久没写题了,还上强度,正常,渐渐找回感觉就好
2.鼠标搞我心态,赶紧换个鼠标
3.基础大概都还在,继续学吧
D题
维护后缀最大值的哈希(方便比较),如果相同就代表平局,输出 YES
如何维护这样一个后缀最大值见 线段树的用法
目前问题在于如何求哈希,并进行合并。
其实好像也差不多?只要能够维护好这个后缀最大值,想要哈希就很简单了。哈希其实就是一个基于某进制的表示
由于是后缀最大值,那肯定是保留 右区间的后缀最大值,然后找到左区间的后缀最大值(要求均大于等于右边区间的最大值),然后重新计算哈希值就好了
对于下面代码中,cnt用于表示这样一个单调序列的长度,h则表示哈希值。
那么在拼接的时候,就要删去原来的,加上新增的。
对于某一个区间的哈希,右区间的序列在高位,左区间在低位,所以是 mk.h -= `````(mk.cnt - mk.lson.cnt),且左区间的序列中的最小的那一个,一定是大于等于右区间序列中最大的那个的,所以左区间一定是满足的,而右区间需要进一步的划分
然后在merge的时候,左边是低位,右边是高位,所以是 右.h * pow[ 总.cnt - 左.cnt] + 左.h
看上去复杂,实际上很简单,多动脑 !!!
E题
今天花了一天?总算是看明白这什么意思了,也有可能是因为我今天太摆了(
线段是yyds
建立在一个很重要的结论之上:
要想更新的次数最少,就把负数全放前面,然后正数从小到大排序放后面,假设更新次数为 minn
要想更新次数最多,就从依次把最小的正数放到负数的前面,假设更新次数为 maxn
关键:可以通过操作,得到从minn 到 maxn 的全部序列
如何得到呢。
我们可以考虑先把负数放在一起,n1, n2, n3, ...
考虑这样一个升序的正数序列,p1, p2, p3, ...
这样组合得到 p1, p2, p3, ...pk, n1, n2, n3 ...,
两个序列的和恰好 小于等于 0,而对于多出的正数,我们将其放在序列的末尾(假设有 L 个),这样得到的可更新次数一定是最多的,也就是 k + L
而如果我们这样排列 n1, n2, n3, ..., p1, p2, p3, ... pk, pk+1, ... ,那么这样更新次数一定是最少的,是 L 个
如何转移,只需要依次将 pi ( i = k, k-1, k-2, ... , 1)移到负数序列末尾就可以了。
这样答案就是 k + L - L + 1
问题是要如何求解这个 k,也就是 前 k 小的正数之和 + 负数 的和 <= 0
这样的问题可以转变成求 前 m 大的正数之和 >= 正数与负数之和
那么,是直接使用了权值线段树,虽然不确定会有多少正数确定,但是上限是确定的,最多4e5个,而正数的范围比较大(1e9),所以这里直接用区间来表示这个数是否存在,比如对于区间 [1,1],就说明 1 这个正数存在,而用 num 来表示区间内的数字出现了多少次,sum表示区间中存在的数字之和。
剩下应该没什么需要补充的了。

#include <iostream> using namespace std; typedef long long ll; const int N = 2e5 + 3; const int INF = 1e9; namespace SegmentTree { struct node { int l, r, num; ll sum; } st[N << 5]; int tot; void update(int &id, int segl, int segr, int pos, int val) { if (!id) id = ++tot; // 离散化? st[id].num += val; st[id].sum += val * pos; if (segl == segr) return; int mid = (segl + segr) >> 1; if (pos <= mid) update(st[id].l, segl, mid, pos, val); else update(st[id].r, mid + 1, segr, pos, val); } int query(int id, int segl, int segr, ll sum) { if (segl == segr) // return (sum <= 0 ? 0 : (sum + segl - 1) / segl); return (sum <= 0 ? 0 : min(st[id].num, (int)((sum + segl - 1) / segl))); int mid = (segl + segr) >> 1; // 要前几大的数,所以先和右边区间比较,如果右边不够,加上左边 if (st[st[id].r].sum >= sum) return query(st[id].r, mid + 1, segr, sum); return query(st[id].l, segl, mid, sum - st[st[id].r].sum) + st[st[id].r].num; } void print() { for(int i = 1;i <= tot;i++) cout<<i << " " << st[i].l << " " << st[i].r << " " << st[i].num << " " << st[i].sum << '\n'; return ; } }; int a[N]; int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, T, rt = 0, cnt = 0; ll sum = 0; cin >> n >> T; for (int i = 1; i <= n; ++i) { cin >> a[i]; if (a[i] > 0) SegmentTree::update(rt, 1, INF, a[i], 1), ++cnt; sum += a[i]; // 总和 } // SegmentTree::print(); for (int kase = 1; kase <= T; ++kase) { int x, v; cin >> x >> v; if (a[x] > 0) SegmentTree::update(rt, 1, INF, a[x], -1), --cnt; // 删数 if (v > 0) SegmentTree::update(rt, 1, INF, v, 1), ++cnt; //加数 sum = sum - a[x] + v; // SegmentTree::print(); // 更新总和 // cnt : 正数的个数 a[x] = v; // cout << sum << '\n'; cout << cnt - SegmentTree::query(1, 1, INF, sum) +1<< "\n"; } return 0; }