牛客多校第七场 题解
B xay loves monotonicity
题意:给定两个长度为 的序列 与 ,,。有如下的修改操作:
- 给定 与 ,令 。
- 给定 ,区间 翻转。
有如下询问:给定区间 ,将 放入集合 中,记集合 中最后一个元素为 ,找到 中满足 且 的最小的 ,并插入集合。记 最终大小为 ,问 中相邻不同的个数。。
解法:首先考虑如何才能统计不连续的 序列的答案。注意到我们需要让每一次的 变大,因而我们可以划定一个下界,只有 在下界之上的才进行统计。
定义一个 cal 函数——cal(l,r,maximum),其中 maximum 为一个 pair 类型,存放了要求的 的下界(即左侧区间的 的最大值)与这个 对应的 。 其计算可以类似线段树,递归的进行。
记待查区间为 。如果左侧区间 的 的最大值不大于要求的值,则左侧区间全军覆没,直接去右侧区间去查询;反之,则在左侧区间查询,同时该大节点 上会存储一个“在大于 最大值的情况下右侧区间 的答案”,累计上这一部分即可。类似的思想还在洛谷 P4198 楼房重建 中有所体现,可以在此题中进一步的加强理解。因而调用一次 cal 函数的复杂度为 。
int cal(int place, int left, int right, pair<long long, bool> num) { if(left == right) return t[place].maximum.first >= num.first && num.second != t[place].maximum.second; pushdown(place, left, right); int mid = (left + right) >> 1; if (num.first <= t[place<<1].maximum.first) return cal(place << 1, left, mid, num) + t[place].right_num;//右侧区间对应的答案存放在right_num中 else return cal(place << 1 | 1, mid + 1, right, num); }
因而,线段树上维护的信息就有—— 上 的最大值与对应位置的 的值; 区间修改的 tag;右侧区间在满足左侧区间最大值的情况下的答案 right_num。
对于两个修改操作——这个是非常常见的线段树操作。只说一下区间合并 pushup 的操作——最大值同朴素的最大值合并,最大值的答案可以直接暴力去查询。
接下来就是查询操作。由于题目的要求,左端点必取,因而先把这一位对应的 与 全查出来;之后将整个查询的大区间根据线段树分成 个区间,之后再对这些区间从左到右使用 cal 函数依次去查询对应的答案。注意向 cal 函数里传入的 maximum 需要不断的和当前区间的最大值做 max 操作。
整体复杂度 。
#include <cstdio> #include <algorithm> #include <vector> using namespace std; struct node { pair<long long, bool> maximum; int right_num; int tag; }; struct node t[800005]; long long a[200005]; bool b[200005]; void pushdown(int place,int left,int right) { t[place << 1].maximum.second ^= t[place].tag; t[place << 1].tag ^= t[place].tag; t[place << 1 | 1].maximum.second ^= t[place].tag; t[place << 1 | 1].tag ^= t[place].tag; t[place].tag = 0; } int cal(int place, int left, int right, pair<long long, bool> num) { if(left == right) return t[place].maximum.first >= num.first && num.second != t[place].maximum.second; pushdown(place, left, right); int mid = (left + right) >> 1; if (num.first <= t[place<<1].maximum.first) return cal(place << 1, left, mid, num) + t[place].right_num; else return cal(place << 1 | 1, mid + 1, right, num); } pair<long long, bool> my_max(pair<long long, bool> &a, pair<long long, bool> &b) { return a.first > b.first ? a : b; } void pushup(int place, int left, int right) { t[place].maximum = my_max(t[place << 1].maximum, t[place << 1 | 1].maximum); int mid = (left + right) >> 1; t[place].right_num = cal(place << 1 | 1, mid + 1, right, t[place << 1].maximum); } void build(int place,int left,int right) { if(left==right) { t[place].maximum = make_pair(a[left], b[left]); return; } int mid = (left + right) >> 1; build(place << 1, left, mid); build(place << 1 | 1, mid + 1, right); pushup(place, left, right); } void update_a(int place,int left,int right,int start,long long x) { if(left==right) { t[place].maximum.first = x; return; } pushdown(place, left, right); int mid = (left + right) >> 1; if(start<=mid) update_a(place << 1, left, mid, start, x); else update_a(place << 1 | 1, mid + 1, right, start, x); pushup(place, left, right); } void update_b(int place, int left, int right, int start, int end) { if(start<=left && right<=end) { t[place].maximum.second ^= 1; t[place].tag ^= 1; return; } pushdown(place, left, right); int mid = (left + right) >> 1; if(start<=mid) update_b(place << 1, left, mid, start, end); if(end>mid) update_b(place << 1 | 1, mid + 1, right, start, end); pushup(place, left, right); } struct interval { int place; int left; int right; }; vector<interval> ask; void divide(int place,int left,int right,int start,int end)//根据线段树来划分区间 { if(start<=left && right<=end) { ask.push_back((interval){place, left, right}); return; } pushdown(place, left, right);//一定记得要进行pushdown操作——虽然只是分区间。但是凡是分区间的,一律pushdown int mid = (left + right) >> 1; if(start<=mid) divide(place << 1, left, mid, start, end); if(end>mid) divide(place << 1 | 1, mid + 1, right, start, end); } pair<long long,bool> query_num(int place,int left,int right,int start) { if(left==right) return t[place].maximum; pushdown(place, left, right); int mid = (left + right) >> 1; if(start<=mid) return query_num(place << 1, left, mid, start); else return query_num(place << 1 | 1, mid + 1, right, start); } int n; int query_ans(int start,int end,pair<long long,bool> num) { ask.clear(); divide(1, 1, n, start, end); int ans = 0; for (auto i : ask)//我们把对应的区间都已经存下来了,只需要一段段的去查询就可以了 { ans += cal(i.place, i.left, i.right, num); num = my_max(num, t[i.place].maximum); } return ans; } int main() { int q, op, l, r; long long x; scanf("%d", &n); for (int i = 1; i <= n;i++) scanf("%lld", &a[i]); for (int i = 1; i <= n;i++) scanf("%d", &b[i]); build(1, 1, n); scanf("%d", &q); while(q--) { scanf("%d", &op); switch(op) { case 1: { scanf("%d%lld", &l, &x); update_a(1, 1, n, l, x); break; } case 2: { scanf("%d%d", &l, &r); update_b(1, 1, n, l, r); break; } case 3: { scanf("%d%d", &l, &r); if(l==r) { printf("0\n"); continue; } pair<long long, bool> head = query_num(1, 1, n, l); printf("%d\n", query_ans(l + 1, r, head)); break; } } } return 0; }
F xay loves trees
题意:给定两棵有 个节点的树,要求找到最大的点集合 ,:
- 在第一棵树上, 是 的祖先或者 是 的祖先。
- 在第二棵树上, 不是 的祖先并且 不是 的祖先。
并且还要求这些点在第一棵树上是连续的。求最大的集合大小。
解法:如果没有连续的条件,则是原题C. Trees of Tranquillity。
容易发现,这些点在第一棵树上构成了一条连续链。因而可以考虑在第一颗树上进行尺取法——划定所属区间的深度最小的节点(下称上界)与深度最大的节点(下称下界)。判定方法可以通过欧拉序列实现,同此题的判定方法,具体的可以参看我这一篇题解题解 CF1528C Trees of Tranquillity,基于第二棵树进行窗口的滑动。
暴力的实现方法是——当前节点必须要插入,对于和当前这一位冲突的,包含冲突的和它的祖先全部删掉。但是这个在极端数据下(例如链+菊花)会退化到 级别。这是因为可能下移一位就导致上界要向下移动非常的多。此题可以通过这一方法通过。
其实我们只需要一个修正就可以了——下界向下移动一位时上界至多只允许移动一位,如果要移动多位则要看下界的下方最长链长能否超过现有链长。如果没有可能,那么直接放弃这棵子树——它一定不会带来更优的答案。因而均摊下来区间下界移动一位上端点也只移动一位,复杂度还是接近 。
具体代码可以参看这个提交:签到题破防 F
还有一种做法——使用可持久化数据结构如主席树,记录以当前节点为上端点其下端点能延申到的最远的区间集合。复杂度 。
J xay loves Floyd
题意:给定一种错误的求全源最短路的方式——中转点在最内层循环。问这样错误的求法可以得到多少个正确的最短路数对。。
解法:首先,正确的全源最短路是一定要求出的,可以使用 次 Dijkstra 在 的时间内求出。
法一:一个较为暴力的方法。我们将正确的最短路对构建成一张新的图,然后直接用题目所给的错误最短路方法去跑,如果碰到正确的就加入到这张新图中。这种方法看似暴力,其实由于大多数边并未加入,因而复杂度是可以得到保证的。
#include <cstdio> #include <algorithm> #include <queue> #include <vector> #include <memory.h> #include <bitset> using namespace std; const long long inf = 0x3f3f3f3f3f3f3f3fll; long long dis[2005][2005], ori[2005][2005]; bool vis[2005], right[2005][2005]; vector<pair<int, long long>> graph[2005]; vector<int> newgraph[2005]; struct node { int id; long long dis; bool operator<(const node &b) const { return dis>b.dis; } }; struct line { int from; int to; long long v; int next; }; struct line que[5005]; int headers[2005], n, book[2005], cnt; void add(int from,int to,long long v) { cnt++; que[cnt].from = from; que[cnt].to = to; que[cnt].v = v; que[cnt].next = headers[from]; headers[from] = cnt; } void dijkstra(int place) { priority_queue <node> q; for (int i = 1; i <= n; i++) { book[i] = 0; dis[place][i] = inf; } dis[place][place] = 0; q.push((node){place, 0}); while(!q.empty()) { node now = q.top(); q.pop(); if(book[now.id]) continue; book[now.id]=1; for (int i = headers[now.id]; i; i = que[i].next) if (dis[place][que[i].to] > dis[place][now.id] + que[i].v) { dis[place][que[i].to] = dis[place][now.id] + que[i].v; q.push((node){que[i].to, dis[place][que[i].to]}); } } } int main() { int u, v, m; long long w; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) ori[i][j] = dis[i][j] = inf; for (int i = 1; i <= m; i++) { scanf("%d%d%lld", &u, &v, &w); ori[u][v] = w;//按错误最短路跑的一张图 add(u, v, w); } for (int i = 1; i <= n; i++) ori[i][i] = 0; for (int i = 1; i <= n; i++) dijkstra(i); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (ori[i][j]<inf) newgraph[i].push_back(j); int ans = 0; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (dis[i][j]>=inf)//正确的都已经是inf了,错误的只会更大 ans++; else for(auto k:newgraph[i]) if(dis[i][j]==ori[i][k]+ori[k][j]) { ans++; newgraph[i].push_back(j); ori[i][j] = dis[i][j]; break; } printf("%d", ans); return 0; }
法二:考虑何时会得到正确的最短路。错误的最短路错误就在于——即使正确的松弛边还没有被松弛,只要中转点被遍历完就更新完毕,以错误的答案继续更新。因而想要正确,就必须保证在更新的时候,正确的最短路中转点出现,同时对应的两条松弛边也已经被松驰过了。因而需要三组 bitset——一组是刻画横向的 ,一组是刻画纵向的 ,还有一组是刻画松弛点。三者取与,如果存在某一个点在三个 bitset 中都取 ,则证明这个点可以被正确的更新。整体复杂度 ,完全可以通过此题。
K xay loves sequence
题意:给定一个长度为 的序列 ,一次操作允许对区间 的全部数在模 意义下加一或减一。 组询问,给出 ,问最少多少次操作能将区间 清零。保证 ,。
解法:首先考虑暴力做法。当 的时候此题退化为【GDOI2018 day1】密码锁(lock)。此题可以在 的时间内完成,若不计排序则为 。考虑每次操作是 区间 整体增加 或减少 ——因而可以考察其差分数组 ,区间归零的等价于区间差分归零。每一次区间修改等价于差分数组一个增加 而另一个减少 。注意到 ,因而一个贪心的做法是对于小的数减,对于大的数加,这样的操作数一定最少。因而首先对 都限制在 的范围——对其取模,然后排序,构造其前缀数组 ,后缀数组 ,这是因为对于大的数,一定是往上累加到 的。只需要找到一个值 满足 ,即证明从这里分界,比 小的数减到 ,比 大的数配合小的数减的操作,加到 ,不会浪费次数,因而答案就是 。这里找到这个 既可以 的遍历也可以 的二分答案——显然这个是满足单调性的。
对于这题,如果直接使用这个方法,则整体复杂度会到达 ,这显然是无法接受的。但是这个方法给了我们一个借鉴——原算法 的瓶颈在于:
- 要 的遍历整个数组以求出 与 。
- 针对于不同的 ,我们都需要排序。
我们可否使用一种数据结构,能够快速的排序,并求出这个数组。后面的二分过程复杂度仅 ,可以保留。因而我们采用了主席树。考虑我们在原算法中需要哪些东西——前缀和与后缀和、元素出现的次数(因为有 的存在),所以主席树中也需要维护这些东西——前缀和与出现的次数。
考虑最重要的二分过程。我们会划出一条大小的分界线出来,大数取 而小数取 。我们可以将这个原本是竖着划出来的线横过来——考察当值为多少的时候取 与 。因而二分的是分界值 。因而我们需要统计大于 出现的数的个数与和、小于 的出现次数与和。
考虑到此处因为 的不同,排序出来的结果也不同。但是我们的复杂度仅允许 级别,甚至都不允许 的遍历负数并对其修改,因而将正数负数分开处理。对于正数,那么在排序数组中出现的就是它本身;对于负数,它在排序的数组中是以 的形式出现的。那么可以考虑直接拿掉这个 ,在负数这里也划出一条线——,作为分界线。在分界线的上方,()对答案的贡献是 ,而在分界线下方则是 。
考虑了分界线上下,再来考虑分界线上的数字——这部分数字会被分割成两部分,一部分是作为小数看待的,而另一部分则是作为大数看待的。而这一部分需要进一步的二分答案来找到真正合适的一个划分方式。
最后要注意的一点就是区间的端点。因为每次给定的是一个区间,因而差分数组的 项在真正操作的时候并不能直接使用——因为修改之后的差分数组在这一项上也并非是 而是 。因而这一项单独判断,直接用 作为差分数组的起点。
整体复杂度 ,来源于二分的界限与主席树的 操作。
#include <cstdio> #include <algorithm> using namespace std; const int N = 200000; const long long inf = 0x3f3f3f3f3f3f3f3fll; struct node { pair<long long, int> value; int leftson; int rightson; }; struct node t[40 * N + 5]; int positive_root[N + 5], negative_root[N + 5]; int cnt; void update(int &place,int old,int left,int right,int num) { t[place].value = t[old].value; if(left==right) { t[place].value.first += num; t[place].value.second++; return; } int mid = (left + right) >> 1; if(num<=mid) { t[place].rightson = t[old].rightson; t[place].leftson = ++cnt; update(t[place].leftson, t[old].leftson, left, mid, num); } else { t[place].leftson = t[old].leftson; t[place].rightson = ++cnt; update(t[place].rightson, t[old].rightson, mid + 1, right, num); } t[place].value.first = t[t[place].leftson].value.first + t[t[place].rightson].value.first; t[place].value.second = t[t[place].leftson].value.second + t[t[place].rightson].value.second; } pair<long long, int> query(int former, int latter, int left, int right, int start, int end) { if(left>end || right<start || !latter) return make_pair(0, 0); if(start<=left && right<=end) return make_pair(t[latter].value.first - t[former].value.first, t[latter].value.second - t[former].value.second); int mid = (left + right) >> 1; pair<long long, int> ans = make_pair(0, 0); if(start<=mid) { pair<long long, int> temp = query(t[former].leftson, t[latter].leftson, left, mid, start, end); ans.first += temp.first; ans.second += temp.second; } if(end>mid) { pair<long long, int> temp = query(t[former].rightson, t[latter].rightson, mid + 1, right, start, end); ans.first += temp.first; ans.second += temp.second; } return ans; } long long a[N + 5], dif[N + 5]; int main() { int n, q; scanf("%d%d", &n, &q); long long maximum = -inf, minimum = inf; for (int i = 1; i <= n; i++) { scanf("%lld", &a[i]); dif[i] = a[i] - a[i - 1]; minimum = min(minimum, dif[i]); maximum = max(maximum, dif[i]); } for (int i = 1; i <= n;i++) { if(dif[i]>=0) { negative_root[i] = negative_root[i - 1]; positive_root[i] = ++cnt; update(positive_root[i], positive_root[i - 1], 0, maximum, dif[i]); } else { negative_root[i] = ++cnt; positive_root[i] = positive_root[i - 1]; update(negative_root[i], negative_root[i - 1], minimum, -1, dif[i]); } } while(q--) { int l, r; long long k, ans = inf; scanf("%d%d%lld", &l, &r, &k); long long left = 0, right = k; while(left<=right) { long long mid = (left + right) >> 1; long long upper = mid, lower = mid - k; pair<long long, int> positive_above = query(positive_root[l], positive_root[r], 0, maximum, upper + 1, maximum); pair<long long, int> positive_below = query(positive_root[l], positive_root[r], 0, maximum, 0, upper - 1); pair<long long, int> negative_above = query(negative_root[l], negative_root[r], minimum, -1, lower + 1, -1); pair<long long, int> negative_below = query(negative_root[l], negative_root[r], minimum, -1, minimum, lower - 1); if(a[l]>mid) { positive_above.first += a[l]; positive_above.second++; } if(a[l]<mid) { positive_below.first += a[l]; positive_below.second++; } long long suffix = (positive_above.second * k - positive_above.first) - negative_above.first, prefix = (negative_below.second * k + negative_below.first) + positive_below.first; int rest = r - l + 1 - positive_above.second - positive_below.second - negative_above.second - negative_below.second; if(suffix<prefix) right = mid - 1; else left = mid + 1; if(rest) { int center_left = 0, center_right = rest; while(center_left<=center_right) { int center_mid = (center_left + center_right) >> 1; long long add_suffix = suffix - center_mid * lower; long long add_prefix = prefix + (rest - center_mid) * upper; ans = min(ans, max(add_prefix, add_suffix)); if(add_suffix<add_prefix) center_left = center_mid + 1; else center_right = center_mid - 1; } } else ans = min(ans, max(suffix, prefix)); } printf("%lld\n", ans); } return 0; }