七点半的灯火,人潮将我吞没,连同我小小的歌。
CF1783E 难点在读题,一句话题意是对于两个元素大小在 以内的序列 ,找到所有的 能够满足 。
假设我们已经对于每一对 都求出了满足条件的 的集合,对他们求交就可以得到整个序列的答案集合,那么只需要考虑怎么对 求所有的 即可。这个条件等价的可以转换成 ,于是可以差分预处理出来不能成为 的倍数的数,于是可以直接枚举这样的 的倍数,检查是否满足条件即可,复杂度 。
CF1783F 考虑单个序列怎么做,这个东西是个置换,把所有的环扣出来即可,那么答案就是 ,其中 是环的个数。
如果两个序列,同时操作需要保证两个环同时还原。再返回去看 的来的过程,操作一次相当于把一个环的点数减一,而每个环都有一个不会动的点,于是可以通过这个不动点去同步两个序列,具体而言,对于 在 中的环记为 ,同样地定义 ,那么将 和 连边,跑最大匹配即可。
CF1749E 一道很有意思的建图题,使得这张图隔断的条件很简单,需要存在一条联通的栅栏,长度为 ,于是建边为: 向斜方向上的点连边,如果该点已经被放置,那么边权为 ,否则边权为 ,然后再弄起始节点,跑 最短路就可以了,复杂度 。
CF1749F 首先是一个很套路的分类,可以把子树内的贡献和子树外的贡献分开来考虑。
但是我并没有第一时间想到,因为受到了 另外一道题 的误导,这一道题大概算是弱化版,只用更新到某个点的距离不超过 的节点的权值,这个时候就可以用 序来维护,因为是一圈一圈的,还非常好写。
也可以发现这两道题本质不同的地方,在于数据范围,并且上一道限制了节点的度数小于 ,就不会被双重菊花图卡爆。
而这一道比较关键的地方在于 ,可以盲猜这个会出现在复杂度里面。 显而易见的是把要把贡献叠到被计算的单点上,再结合第一个分类,往子树上贡献可以搞一个数据结构解决,往子树外贡献可以非常平凡的 暴力爬。
现在的问题是会算重,我想了半天就是不会,流汗。
接下来是形式化的题解。
定义 为与 结点距离为 的在 的子树内的节点的增值。
query
:
update
: 是一个迭代的过程,当前在节点 ,需要更新的距离为 ,那么重复以下操作 。
然后把这个玩意搬到一条链上面,套上一个重链剖分,用树状数组维护就可以了。
CF1743F 感觉比 E 要难,但是评分低了。 拿到题感觉下不了手,观察到这个集合最大也只有 个元素,那么可以套路地去给每一个元素计算贡献。
还是不会。。。
其实发现我们关心的是这个元素和当前操作集合是否有包含关系。分类讨论一下 和 的关系。
包含,那么在交和并操作之后依然包含。
不包含,那么在异或和并操作之后可以使得包含关系成立。
如果这个元素最后一次是被包含在了 那么对于之前所有集合操作方式有 使得这个元素被包含,对于之后的序列有 中操作序列,乘起来便是整个贡献。
现在我们需要求到这个 ,于是我们从前往后依次枚举每一条线段,每次查询 中 的个数,直接累加贡献,然后把区间赋 。 注意要离散化或动态开点。
CF1821F 挺好一道题,可惜我是暴力二项式反演丑陋做法。
贪心的判定很显然,能往左边倒就往左边倒。先按计数 dp 方法写个式子, 表示前 棵树最后一棵倒下来放到了 的方案数。
然后我对 GF 不怎么熟,就观察了一下这个式子,发现可以改写为
继续观察,发现可以转化这个问题,对于一个长度为 的序列分成 段,使得每一段的长度都大于等于 ,当一段的长度大于 时,该段的贡献为 。
于是定义 表示恰好有 段的长度大于 的方案数,则答案为 ,然后开始吟唱,定义 表示至少 段的长度大于 ,那么有下面的等式。
继续化简答案
于是可以 直接计算了。
其实这个玩意没有这么复杂。可以有一个非常优美的 GF 做法,可惜我不会 /kk
CF1651F 高妙题,第一眼看上去是对塔分块,有一个比较暴力的 的做法能冲过去。
正解就很神了,首先需要观察,一个怪兽能造成的贡献可以拆分成两种方式,第一种是一段区间直接推平,第二种是对于某一个塔削弱血量。考虑颜色段均摊,维护场上的若干个连续段 。
如果遇到 ,直接暴力算。遇到 $l这代表被以前的询问推平的一段区间,问题成了给定初始生命 ,时间差 ,左右端点 ,问是否能再次推平这个区间,或者是在这个区间停下。
比较关键的步骤是将塔的回血操作看成分段的一次函数,具体是
以位置为下标,可以上可持久化线段树,合并一次函数就行了。于是得到了一个主席树上二分的单 做法。
CF1657F 明显是 2-sat
首先把路经抠下来,路经上第 个点可以是 或 ,为表述方便,记为 ,将 视为一个限制。
现在考虑如何合并两个限制,记另外一个限制为 ,讨论一下。
:如果选 ,则强制另外一个限制路径用 满足限制,如果选了 ,则只能选
:如果选 ,则强制另外一个限制路径用 满足限制,如果选了 ,则只能选
对于 ,同理,不写了
于是这是个点数为 的做法。
CF1661F 令 表示该区间长度为 ,增加 个传送器后的代价。
观察一下,这个是个均值,所以每段最好分得尽量平均,有
再观察一下,发现 有单调性,可以瞪眼大法知道是不增。然后可以直接二分这个减少的量记为 ,于是对每一段都求出来最大满足 的 ,于是增加 个传送器的代价可以求得记为 。
似乎直接去求最大满足 的 即可,但是这个 不一定最优,我们可以在 的基础上继续增加,因为每增加 个传送器,代价就会减少 ,所以算出来最少增加 个。
CF1809F 首先看到 ,这明显是一个值得去发现性质的点。因为油箱容量不变(废话),所以遇到 能加满就加满,只有在实在没油的情况下才会去加 。发现这个合并并不是那么优美,因为每次剩下的油量都不一样,那么考虑每次都在油箱空了的时候去合并。
如果当前
并且 ,直接交完当前需要的油钱,移动到下一个位置。
否则后面将会有 个连续的 ,对于这一段区间记为 ,记 的前缀和数组为 ,需要二分出第一个 ,使得 ,那么这一段区间的所有油都应该在 处只花 的代价加上,这是就会出现后面一段 的油不够,那么这一段只能花 的代价买油。
如果当前 ,直接交 ,往下移动。
于是得到了从每一个点出发走一步在最优情况下能够到达的位置记为 ,显然这是一个倍增数组。
最后会发现一个小问题,在倍增完之后有可能到不了终点,并且这一段是连续的 ,那么直接计算两者之间的距离就能解决了。
CF1697F 又是 2-sat
,首先建出来 的边表示 是否大于等于 ,有下面几个限制
注意到不能忘记去建逆否命题的边,以及 。
CF1680F 首先二分图一定满足题目要求,否则该图中有奇环,删去的边一定要一键干掉所有的奇环,那么树上差分维护经过每个点的奇环就可以了。
CF1716 F 就是要求下面这个式子,开始吟唱
CF1709F 好吧,根本没读懂题,所以给一个看到的正常翻译。
一棵高度为 的满二叉树 (共 个点, 条边),边从父亲指向儿子。
每条边的最大流量为 内的整数,求有几种赋最大流量方案,使得以根为源点,所有叶子为汇点,最大流恰好为 。
令 表示现在从下往上流回 第 层,最大流量是 的总方案数,因为每一层的 值是一样的,所以说只需要对 个点进行计算就可以了。那么往上计算,对于两个子结点 往 转移的时候,限制的因素要么是 处的流量,要么是 和 的流量和。
写出转移方程
注意到 和 是一个东西,于是 。于是调整一下这个式子变成
因为 是个卷积形式。
那么令 是上一层的生成函数, 是这一层的生成函数,那么 ,直接 NTT
解决即可。
CF1795F 考虑对于一个固定的步数,构造出来一个唯一的方案。
操作是有顺序的,必须按照 的顺序走,那么在总步数为 的约束下,每一个棋子的步数可以确定,记为 ,于是递归去处理这个问题,记每一个位置目前能够往下(深度更大)走的最大距离为 对于当前位置的 ,如果 ,直接把它放下去,否则往上挪,如果父结点也有棋子或者它本身是根,那么无法完成要求。于是二分即可。
CF1809G 每次在末尾添加一个元素的时候,关心的只有前面所有元素的最大值,因为 ,并且不可能去记录现在剩下来的最大值,所以猜测这个计数往后添加时一定是有一个明确的顺序的。
考虑到这个状态只能一维地去定义,那么就必须保证每一步填完之后这个数列都合法,就是说 要让当前数列开头的数和剩下的数无论怎么填都合法 ,定义 表示已经填了前面第 大的元素,那么第 大的元素有两种填法。
不成为第一个数,
成为第一个数,这样会有冲突,是 连续的一段应该也一起放,这样保证状态还是合法的。
CF1814F 线段树分治。判断是否和 联通,直接在并查集上打标记就可以了,注意下传的时候撤销。
CF997C 考虑容斥,定义 表示至少有 行, 列被染成了同样的颜色的方案数。 分类讨论一下,得到
进行一个二维的反演,定义 表示恰好有 行, 列被染成了同样的颜色的方案数。
用总方案数减去 就是答案,现在来求 。
于是可以 计算了。
CF679E 从最难处理的操作入手,如何判断一个区间中存在 的次幂?如果单纯地去维护每个数的真实值,则会出现问题:没有一个统一的标准检验是否是 ,因为每个数处于不同的由 的次幂分割出来的区间之中。 转换一下,维护每个数与它最近的 次幂的差值,这样我们只需要检验数列中是否存在 就行了。 出现另外一个问题,在重复做 3
操作的过程中有可能出现负数,这意味着这个数跨越了一个 的次幂,我们必须单独去更新它。 考虑这个做法的复杂度, 的次幂在可能的值域中 最大只能在 次方左右,也就是说,每个数最多只会跨过 次,但是做 2
操作会出现 个连续段,增加 的势能,结合到每次操作是 的,所以总的时间复杂度是 。
CF1588F 好神的题,但也并非没有突破口。 发现这里既有图上的又有序列上的操作,非常抽象🤔,思考能不能分块,对谁分块。 因为操作过于复杂,那就对操作分块,于是我们现在得到了 个块,每个块里面有 个操作,我们需要在 左右的复杂度内解决每个块里的询问。 最恶心的操作是操作 3
,出现了改边。结合到分块的原理,块内的元素相当于是等价的,他们的行为是平行的,而在操作 3
里面出现的点都经历了改边的步骤,所以他们的行为一定互相不平行,钦定他们为关键点,记换上的一部分为 ,其中 和 是被标记的关键点,那么可以知道 可以放在一个块里面, 可以放在一个块里。 就像是这样
怎么想到的,为啥是对的? 其实很明显,因为 有改边的操作,那么对于环上连在 后面的点来说, 可以作为一个 leader, 后面的点的行为都是由 指挥进行的。简单看一下,对于每一次拎出来的 操作,最多会有 个 leader 出现,意味着块的个数是 级别的。 这样看起来复杂度就对了,于是思考操作一怎么做,可以拆开一个询问,把区间和变成前缀和做差,那么只需要统计每个块中有多少个点出现在了询问中即可,这一步是 的。 操作二直接跳环上关键点,这一步是 的,操作三直接改即可。 因为 同阶,所以总的复杂度是 的。
代码确实不好写,还要多看几遍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 #pragma GCC optimize("Ofast,no-stack-protector" ) #pragma GCC target("avx2,fma" ) #include <cmath> #include <cstdio> #include <vector> #include <cstring> #include <iostream> #include <algorithm> #define LL long long #define ULL unsigned long long using namespace std;const int MAXN = 2e5 + 5 ;const int MAXM = 600 ;template <typename T>inline void read (T& x) { x = 0 ; int f = 1 ; char c = getchar (); while (c < '0' || c > '9' ) { if (c == '-' ) f = -f; c = getchar (); } while (c >= '0' && c <= '9' ) { x = (x << 3 ) + (x << 1 ) + (c ^ 48 ), c = getchar (); } x *= f; } template <typename T, typename ... Args>inline void read (T &x, Args&... Arg) { read (x), read (Arg...); }template <typename T>inline T Abs (T x) { return x < 0 ? -x : x; }template <typename T>inline T Max (T x, T y) { return x > y ? x : y; }template <typename T>inline T Min (T x, T y) { return x < y ? x : y; }int n, q, m, cnt, siz, ind, tp,nxt[MAXN], col[MAXN], ed[MAXN], stk[MAXN];struct Node { int opt, x, y, ind; } que[MAXN], ask[MAXN];vector<int > pos; bool vis[MAXN];LL sum[MAXN], add[MAXN], num[MAXM][MAXM], a[MAXN]; inline void solve (int m) { ind = 0 , cnt = 0 , pos.clear (); memset (vis, 0 , sizeof (vis)), memset (col, 0 , sizeof (col)); memset (num, 0 , sizeof (num)), memset (add, 0 , sizeof (add)); for (int i = 1 ; i <= m; i++) { if (que[i].opt == 1 ) { if (que[i].x > 1 ) ask[++cnt] = Node{-1 , que[i].x - 1 , 1 , i}; ask[++cnt] = Node{+1 , que[i].y, 1 , i}; } else if (que[i].opt == 2 ) vis[que[i].x] = true ; else vis[que[i].x] = vis[que[i].y] = true ; } for (int i = 1 , now; i <= n; i++) if (vis[i]) { pos.push_back (i), now = i; do { stk[++tp] = nxt[now], now = nxt[now]; } while (vis[now] != true ); ed[pos.size ()] = stk[tp]; while (tp) col[stk[tp--]] = ed[pos.size ()]; } sort (ask + 1 , ask + 1 + cnt, [](const Node& x, const Node& y) { return x.x < y.x; }); for (int i = 1 ; i <= cnt; i++) { while (ind < ask[i].x) add[col[++ind]]++; for (int j = 1 ; j <= (int )pos.size (); j++) num[ask[i].ind][j] += ask[i].opt * add[ed[j]]; } memset (add, 0 , sizeof (add)); for (int i = 1 , now; i <= m; i++) { if (que[i].opt == 1 ) { LL res = sum[que[i].y] - sum[que[i].x - 1 ]; for (int j = 1 ; j <= (int )pos.size (); j++) res += num[i][j] * add[col[ed[j]]]; printf ("%lld\n" , res); } else if (que[i].opt == 2 ) { now = col[que[i].x]; do { add[now] += que[i].y, now = col[nxt[now]]; } while (now != col[que[i].x]); } else swap (nxt[que[i].x], nxt[que[i].y]); } for (int i = 1 ; i <= n; i++) a[i] += add[col[i]], sum[i] = sum[i - 1 ] + a[i]; } int main () { read (n); for (int i = 1 ; i <= n; i++) read (a[i]), sum[i] = sum[i - 1 ] + a[i]; for (int i = 1 ; i <= n; i++) read (nxt[i]); read (q), siz = sqrt (q); for (int i = 1 ; i <= q; i++) { m++, read (que[m].opt, que[m].x, que[m].y); if (m == siz) solve (m), m = 0 ; } if (m > 0 ) solve (m); return 0 ; }
CF1458D 从操作的限制入手,要求是“连续子串必须包含同样数量的字符 0
与 1
”。 将 换成 ,然后记录前缀和,并建图,对于每一个 ,都建一条 的边,如果 这一段能够操作,那么 。 之后的方法就很妙了,如果我们对 操作,那么 会发生改变,似乎没办法维护,但是观察性质可以《发现》:如果说操作前前缀和变化的顺序是从前往后,那么操作之后前缀和的变化是从后往前。 把这个放到图上去,相当于把 这一段里面的所有边都反向。于是现在得到的问题就变成了求一条字典序最短的欧拉通路,贪心很显然。
CF827F 拿到题的第一时间注意到了不能原地停留,而每条边有出现的时间区间,马上想到可以反复横跳。 于是要分奇偶讨论,对于每个点记录 表示最晚能够在 的时间,因为每条边有特定的通过时间,考虑以 升序排序,去掉时间这一维度。那么 一定是由时间更小的边走过来的,在当前边出现的时候是一定能走的。 对于有一些目前还不能走的边 ,可以把它挂在 这个点上面,直到 之后再将它放到边集里面去。因为每条边最多会被塞到边集里面两次,所以复杂度是 的。
CF1307F 不难想到要把点的询问挂到对于它最优的休息点上去,于是预处理每个休息点能够辐射到的范围,发现有一些休息点能够互相覆盖,希望将他们塞到一个并查集里面去。 那么有了第一步,从每个休息点出发走 步,记录每个点最近的休息点,并将能够互相走到的点塞入一个并查集中。 说得有点抽象,代码如下
1 2 3 4 5 6 7 8 9 10 11 inline void spread () { for (int u : sp) que.push (mp (u, 0 )), pre[u] = u; while (!que.empty ()) { pii now = que.front (); que.pop (); int u = now.first; if (now.second >= k >> 1 ) continue ; for (int v : G[u]) { if (!pre[v]) pre[v] = pre[u], que.push (mp (v, now.second + 1 )), dsu.unionset (v, pre[u]); else dsu.unionset (pre[v], pre[u]); } } }
然后考虑回答每一个询问,对于二元组 ,若 那么可以直接走到,否则需要经过休息站。于是从 往 跳 步到 ,从 往 跳 步到 。如果 能够互通,那么 能够互通。 接下来论证这个方法的正确性,即是论证 能走到的有用的休息站集合和 能走到的有用的休息站集合等价,只需要想,如果有一个休息站 , 能走到, 不能走到,这就意味着 ,因为 ,所以先走到 再走到 只会更劣,同理 也能得出一样的结论。
这里有一些小细节需要注意,为了避免小数距离,可以将 乘一倍,在边之间塞虚点;如果往上跳的时候 ,相当于让 往上面跳 步。
CF1556G 删除不是很好实现,首先时光倒流,转换成添加操作。
第一步很难,需要把数放到线段树结构上去做。 可以发现一棵子树内都是联通的,于是可以用这棵子树的根节点代表整棵子树进行操作。
在线段树上记录每个点被删除的时间,可以直接打标记实现,然后遍历每一个不是“叶节点”的点,将其内部暴力连边,若该边连接了 两个点,那么该边出现的时间是 ,并将它加入此时间的边集里。 处理询问时,倒序往前把删掉的边塞入并查集,每次查询只需要问能代表 的节点是否联通即可。
CF1383E
CF678F 如果没有删除,直接李超树上就可以了。考虑离线,线段树分治来做,维护每对 出现的范围,直接给每个叶子节点建一颗动态开点李超树。 因为时间比较久远通过代码头部注释内容判断,放个代码自己复习一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 int n, cnt, re[MAXN], pos[MAXN];struct Range { int l, r; } ra[MAXN];struct Opt { int k, ind, x, y; } opt[MAXN];struct Line { int x, y; } li[MAXN];inline int getval (int ind, int k) { return k * li[ind].x + li[ind].y; }struct LichaoTree { int tot, root[MAXN * MAXM]; struct Node { int l, r, dat; } s[MAXN * MAXM]; inline void insert (int & p, int l, int r, int ind) { if (!p) p = ++tot; if (!s[p].dat) return s[p].dat = ind, void (); int mid = (l + r) >> 1 ; if (getval (ind, mid) > getval (s[p].dat, mid)) swap (s[p].dat, ind); if (getval (ind, l) > getval (s[p].dat, l)) insert (s[p].l, l, mid, ind); else if (getval (ind, r) > getval (s[p].dat, r)) insert (s[p].r, mid + 1 , r, ind); } inline int query (int p, int l, int r, int k) { if (!p) return -INF; if (l == r) return getval (s[p].dat, k); int mid = (l + r) >> 1 , res = getval (s[p].dat, k); if (k <= mid) res = Max (res, query (s[p].l, l, mid, k)); else res = Max (res, query (s[p].r, mid + 1 , r, k)); return res; } } lt; struct SegmentTree { struct Node { int l, r; } s[MAXN << 2 ]; inline void build (int p, int l, int r) { s[p] = Node{l, r}; if (l == r) return pos[l] = p, void (); int mid = (l + r) >> 1 ; build (p << 1 , l, mid), build (p << 1 | 1 , mid + 1 , r); } inline void update (int p, int l, int r, int ind) { if (s[p].l >= l && s[p].r <= r) { lt.insert (lt.root[p], -LIM, LIM, ind); return ; } int mid = (s[p].l + s[p].r) >> 1 ; if (l <= mid) update (p << 1 , l, r, ind); if (r > mid) update (p << 1 | 1 , l, r, ind); } } seg;
CF1149C 首先考察一下括号序列和树的直径的关系,给左括号赋值为 ,给右括号赋值为 ,设 分别为树的直径的两个端点,结合图看一下发现
以三角形符号代替的子树都会自行内部括号匹配消化,于是直径就相当于把括号序列中匹配的中间匹配的括号扣掉之后不能被匹配的括号数。
相当于找一条分界线,求右边的最大字段和,求左边的最小字段和,然后做差。 因为我会 GSS 3 - Can you answer these queries III ,所以这道题我一眼秒了,然后 WA * 3。因为有点细节,放一下部分代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Element { int dat, lmax, rmax, lmin, rmin, sum, maxl1, maxl2; inline Element operator + (const Element oth) { Element res; res.sum = sum + oth.sum; res.lmax = Max (lmax, sum + oth.lmax), res.rmax = Max (oth.rmax, oth.sum + rmax); res.lmin = Min (lmin, sum + oth.lmin), res.rmin = Min (oth.rmin, oth.sum + rmin); res.maxl1 = max ({maxl1, -sum + oth.maxl1, oth.lmax + rmax * 2 - sum}); res.maxl2 = max ({oth.maxl2, oth.sum + maxl2, -rmin + oth.sum - 2 * oth.lmin}); res.dat = max ({dat, oth.dat, maxl2 + oth.lmax, -rmin + oth.maxl1}); return res; } }; Element L = Element{1 , 1 , 1 , 0 , 0 , 1 , 1 , 1 }, R = Element{1 , 0 , 0 , -1 , -1 , -1 , 1 , 1 ,};
CF631E 假设现在交换了 两个位置,记一个前缀和 ,操作后变化的代价是 ,拆一下变成 ,可以直接李超线段树做了。
CF932F 写个式子先,定义 表示从 跳出去的最小代价,那么有
直接李超树合并即可。
CF1860E 难过,赛时写了好久都没有过。😔
其实思路很简单,把相邻两个字符拎出来映射一下,但是边数过多,可以达到 级别,于是把边权扩倍,建虚点即可。 跑 0/1
最短路就行了。
CF1860F 好题,但是没啥人补。 观察一下式子的形式 ,给定了一堆 ,现在要求满足要求的 ,可以放到平面上,把这些二元组看成向量。 于是相当于按 和 的点乘结果排序,于是现在假想一个动态的过程,一个向量绕着原点转一圈,查看是否能满足条件。 于是对于两个点对 和 ,我们只用关心在某个时刻,他们的顺序会交换,那么拎出来每个会产生交换的向量 ,数量级是 的可以接受,将它们按极角排序,思考一下,可以想到向量 每变动一下产生修改的区间是一个连续的段(按该向量上的模排序),检验的方法很典,左右括号赋值,检验是否有前缀值小于 即可。 于是此时直接将修改过的点拎出来,重新排序,然后塞回去即可,动态维护前缀值,只要小于 的前缀个数为 即可。 官方题解的代码非常优美,可以反复读一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 int n; read (n);vector<Bracket> p (n << 1 ) ;for (int i = 0 ; i < (n << 1 ); i++) { read (p[i].a, p[i].b); char c; cin >> c; if (c == '(' ) p[i].c = +1 ; else p[i].c = -1 ; } map<Point, vector<int >> opt; for (int i = 0 ; i < (n << 1 ); i++) { for (int j = 0 ; j < (n << 1 ); j++) { int x = p[i].b - p[j].b; int y = p[j].a - p[i].a; if (x <= 0 || y <= 0 ) continue ; opt[{ x, y }].push_back (i), opt[{ x, y }].push_back (j); } } opt[{ 1 , INF }]; vector<int > ind (n << 1 ) , reind (n << 1 ) ;iota (all (ind), 0 );sort (all (ind), [&](int i, int j) { LL di = dot ({ INF, 1 }, p[i]); LL dj = dot ({ INF, 1 }, p[j]); if (di == dj) return p[i].c > p[j].c; else return di < dj; }); for (int i = 0 ; i < (n << 1 ); i++) reind[ind[i]] = i;int neg = 0 , now = 0 ;vector<int > bal (1 , 0 ) ;for (int i : ind) now += p[i].c, bal.push_back (now), neg += now < 0 ;bool res = neg == 0 ;vector<int > pre; for (auto it : opt) { vector<int > tot = pre; for (int x : it.second) tot.push_back (x); sort (all (tot), [&](int x, int y) { return reind[x] < reind[y]; }); tot.resize (unique (all (tot)) - tot.begin ()); for (int x : tot) neg -= bal[reind[x] + 1 ] < 0 ; vector<int > tmp = tot; sort (all (tmp), [&](int i, int j) { LL di = dot (it.first, p[i]); LL dj = dot (it.first, p[j]); if (di == dj) return p[i].c > p[j].c; else return di < dj; }); vector<int > nind (tot.size()) ; for (int i = 0 ; i < tot.size (); i++) nind[i] = reind[tmp[i]]; for (int i = 0 ; i < tot.size (); i++) reind[tot[i]] = nind[i]; for (int x : tot) bal[reind[x] + 1 ] = bal[reind[x]] + p[x].c, neg += bal[reind[x] + 1 ] < 0 ; if (neg == 0 ) { res = true ; break ; } pre = it.second; } if (res) printf ("YES\n" ); else printf ("NO\n" );
CF1858E1 比较简单,赛时就过了,一个经典的套路,把操作序列建建成树,回退相当于往祖先跳,跳 步,这里倍增处理即可,统计答案用莫队的套路。
CF1858E2 第一想法是在 E1 的基础上加一个主席树,但是空间爆炸了。 其实如果没有 E1,我估计就会做 E2 了。😭 官方题解给的是开 个 set
的弱智做法,本质上等同于主席树,但是不知道为啥空间能过,时间复杂度是 的。 看了 jiangly 的代码,是 的线性牛逼做法,思路其实也很简单。
考虑一个字符如何贡献,我们关心的是出现了多少字符,意思就是字符集的大小,具体出现了多少次并不重要,我只需要知道每种字符第一次出现的位置就行了,很显然,这道题的所有操作都是在尾部进行的。
统计答案可以前缀和,维护操作序列 ,保证 的前 位是数字序列 ,加数非常简单,在末尾添加,这里注意第 位可能之前就有值(回滚回来到 的),做法是先删掉 的贡献,然后添加当前需要加入的贡献。注意将此次操作修改到的元素加入栈中,方便还原。
1 2 3 4 5 read (val);a.push_back ({ val, pos[val], b[n], pos[b[n]], s[n + 1 ] }); if (pos[b[n]] == n) c[pos[b[n]]] -= 1 , pos[b[n]] = q, c[pos[b[n]]] += 1 ;if (pos[val] > n) c[pos[val]] -= 1 , pos[val] = n, c[pos[val]] += 1 ;s[n + 1 ] = s[n] + c[n], b[n] = val, n += 1 ;
删除操作直接回退就好了,也要将修改的信息丢入栈中。 建立在删除和添加操作上的撤销操作其实很容易,类似于可撤销并查集,从堆顶取出修改的信息,倒着做一遍操作,直接还原即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (opt == '+' ) { read (val); a.push_back ({ val, pos[val], b[n], pos[b[n]], s[n + 1 ] }); if (pos[b[n]] == n) c[pos[b[n]]] -= 1 , pos[b[n]] = q, c[pos[b[n]]] += 1 ; if (pos[val] > n) c[pos[val]] -= 1 , pos[val] = n, c[pos[val]] += 1 ; s[n + 1 ] = s[n] + c[n], b[n] = val, n += 1 ; } else if (opt == '-' ) read (val), n -= val, a.push_back ({ -1 , val });else if (opt == '!' ) { auto [x, y, z, w, t] = a.back (); a.pop_back (); if (x == -1 ) { n += y; } else { n -= 1 , b[n] = z, s[n + 1 ] = t; if (pos[x] != y) c[pos[x]] -= 1 , pos[x] = y, c[pos[x]] += 1 ; if (pos[z] != w) c[pos[z]] -= 1 , pos[z] = w, c[pos[z]] += 1 ; } } else printf ("%d\n" , s[n]);
CF1859E 好麻烦的题,做的时候想太复杂了。 表示前 位,已经有了总长度为 的段的最大值。 暴力转移是 ,其中 表示当 划分成一段时的贡献。 考虑优化,注意观察如果 能够转移到 那就说明 ,于是把 即是说差值相同的归为一类。拆拆绝对值就能转移了。
CF1859F 看了 jiangly 传在 b 站上的视频,他用了点分治?? 遂查看官方题解,然后觉得评分确实虚高了。
其中一条 Hint 给得比较直接,它让我想想最优的路径长啥样子(Can you think how would an optimal answer path look like?)。
首先观察到学习的次数一定小于 次,具体问题是学多少次,在哪里学?学多少次的范围是可以枚举的,于是解决在哪里学即可。
这下 Hint 就起作用了,如果在 的路径间有学习点,一定会到最近的学习点学满枚举的次数,然后走剩下的路程,那如果没有学习点直接在路径上呢? 注意到题目允许多次走过某条路径,所以现在的路径看上去像是
会在路径上某个点离开向子树内走到一个学习点并返回,蓝色的路径是原长,红色的路径是在学习之后需要走的长度。
这就启发我们首先处理出来,每个从某个点出发学习 次然后返回该点的最短路径,这一步多源最短路即可解决,记其为 ,在学习了 次的情况下,从根节点出发到当前点的距离 。
考察一条路径的组成,无非两种情况 在 上找一个点 出发去学习 次然后返回路径经过 走到 。 写出式子 。 而第二种情况反过来就变成第一种同样处理。
CF1856E2 😓,bitset 优化背包,但是我真的不会写,希望我多看看这份代码。
Accepted
CF1849F 讨论区有好多神仙做法%%% 但是还是选择看完了官方题解,因为里面有很多经典技巧(classical techniques)。
首先建出来这张完全图,对于 以 的代价连边,划分集合就相当于对每一个点黑白染色,连接了相同颜色节点的边将会按连接的点的颜色计入代价计算之中。考虑当前二分了一个答案 ,那么对于所有边的权值小于 的边,他们一定连接了不同颜色的点,于是判定二分图即可,然而不是很好加速这个进程。从贪心的角度去想,按边权从小往大加入,判断当前是否还是二分图即可,问题在于此时有 条边,但是事实上,很多边是没有必要的,没有必要的含义是在加入这条边之后并不会影响整张图的状态(形态是否还是二分图或是否影响点的染色)。如果加入一条边连接了两个在同一连通块里面的点,出现下面这两种情况
这两个点颜色相同,这张图不再是二分图。
否则不会影响染色。
于是我们不会心一条连接在同一个连通块里面的边,每次都加入最小的连接不同连通块的边,或是说当前边打破了二分图的状态,停止操作。 这个过程很类似于 Kruskal,于是直接上异或最小生成树,在这个基础上染色就行了。
CF1864H 这是官方题解翻译,因为我也不会这道题,所以先翻译一遍。 /kk
首先有一个基础的 的 dp 解法。 在开始之前,令 ,因为在后续会有一个形如 的几何级数会被频繁使用到,并将它记为 。
令 为表示从 开始移动到大于等于 的期望步数,显然,方程如下
很容易可以看出来,区间 中所有的 都是 。
如果对于一个区间 ,它已经可以被一些 的和表示出来,那么它可以被在 区间中更少的一些 所表示。
首先,这里有 。所以 可以被表示为 。
类似地,可以得到
换元一下,有
因为对于表示出 的 已经完全知道,所以可以很快计算出 ,于是 的第一项可以被写出来。
然后对于 的最后一项 是一个平凡的几何级数,所以运用开始定义的 可以写成 。
对于第二项 ,我们已经假设 为 ,所以把它拆开成如下的形式:
有一个显然的结合律,,它的几何级数和递归次数成线性关系。
对于 ,可以被表示成 ,然后可以递归至能够生成 ,得到答案。
复杂度取决于实现方法,为 或者 。
CF1868C 观察到 ,所以考虑枚举每种 。有最大值在其中,充斥比较好做,于是定义 表示对于每条路径,路径上的点的最大值小于 的方案数量。 考虑子树如何拼答案,拆成左子树、右子树和根即可,这样满二叉树可以提前处理出来。
CF1863F 两个性质:
考虑能被保留的区间之间的互相转移, 这个区间能从 转移过来当且仅当 。 可以 转移。
CF1863G 比较有意思。首先把 的边建出来,把交换操作放到基环树上面去。 形象一点理解,相当于把 接在 上面,同时 变成自环,并且 在后续无法继续操作,于是对树的形态计数。先不考虑环,那么方案数为 ,至于环上的点,需要去重,方案数为 。
CF1864E 首先考虑有两个数 ,什么时候能猜出大小关系。
当 ,并且 ,步数为 。
当 ,那么必须把 的位数都猜一边。
当 ,并且 ,步数为 。
于是往 Trie
上塞即可。
CF1864G 非常神奇的题。 记操作了某一行后数字能回到正确的列上的行和操作了某一列后数字能回到正确的列上的行和列是可操作的。 如果当前同时存在可操作的行和列那么一定无解,因为这个时候会有一个格子会被操作两次以上。 结论是记录当前可操作的行、列,只要此时只存在一种,那么直接操作完需要操作的行、列即可。
CF1817C 一开始居然把题看错了,脑子有点晕。😵
现在把题搞清楚了,我来试试
6,推不动了。其实直接代值进去即可。 令
然后可以递推了。😂
CF512D 注意到这个操作有点类似于拓扑排序,唯一的不同是该操作在点的度为 的时候就可以删去。 并且对于每个 都要进行计数,那么考虑能否以上一层的答案来转移当前层。
观察样例发现有 的出现,因为某些点根本删不掉,首先处理出来这些点,具体地,拓扑排序一次找出环上的点,然后把每个环看作一个已经连好的连通块。
因为 比较小,可以想一些状态限制较多的做法,继续之前的想法,观察这张图,发现该图由很多颗树构成,每一颗树的答案是孤立的,如果树连在了环上,可以直接断掉,变成有根树,这样并不影响答案。于是先考虑一棵树计数。
如果是有根树,那么直接做一遍即可,如果是无根树,需要以每个点为根做一次,这样一种方案会被算 次,树上转移方程是平凡的
然而我写了很久,果然我写有细节的代码能力欠缺。好吧,这个确实写起来有点恶心,我写了 3.4k 😭。
CF516E 首先判无解,只有当两个序列长度一样并且不是每个位置上都有 就无解。
感觉很抽象,完全没有思路,留个坑,明天来填。
CF526G 首先一个很明显的观察是,选择的路径一定是从一个叶子到另一个叶子,显然,其中一定包含了直径。
注意到强制包含 这个奇妙的限制。
CF573E 从一个 naive 的想法开始入手,定义 表示前 个数选择了 个数能取到的最大值,那么转移方程为
直接转移是 的,观察这个式子有何性质?
固定下 ,把转移式子里面的两个转移式写成函数的形式记为 ,。这两个函数有交,考虑去二分这个交点然后更新答案。
于是得到现在的做法为维护一个序列 ,初始没有数,每次二分找到两个函数的交点,记作 ,在后面添加一个等于 的元素,并把对 以后的数加上一个等差数列。写平衡树上二分的话,复杂度是 ,一个比较优秀的写法是维护差分数组。确实非常好写。
CF576D 题面看起来很经典,从限制入手,如何转化至少走过 条边?可以直接按照 排序,维护转移的邻接矩阵和当前能够到达的位置,那么 和 之间的转移直接做快速幂即可,注意到只关心可达性,于是可以用 bitset
优化矩乘。
CF1697E 怎么思维又开始僵了啊。 首先对于每个 处理出来 表示该集合里面的点 满足 ,这样的话,在条件二的限制之下一定有:如果 的颜色不是唯一的,那么 就应该和 同色,于是这时从 向 里面的所有点连边,拿到一张有向图。 对于每一个 作为起点,开始遍历,那么能遍历到的点都应该和 染上同样的颜色,如果染色集合里面存在了某对点 ,之间没有连边,就称这个 是孤立的,此时 必须单独一种颜色,否则称遍历到集合为合法集合。 于是对于一个合法集合,就有两种染色方式,一种是染上 种,另一种是全部染上 种,对于一个不合法的集合,就只能全部拆开,于是这是个背包。 然后组合数学算一下即可。
CF1681E 只能说有点疲倦了。 这个就是 ddp 的板题,写代码就当是提一下神。
CF1633E 怎么今天状态也很糟糕啊。熬夜太影响第二天了,况且半夜也调不出来代码,不知道我昨晚在想啥。/fn 这个一眼看上去就是 slope trick
,注意到 ,这也太送了吧。很明显,在 Kruskal
的过程中影响边的选择的是边权的相对大小,于是直接处理出来某两条边在哪个时刻会交换大小。这样会有 个时间点,然后比较牛逼的是直接处理每个时间点的答案,这样是 ,总之可以过就是了。
从这道题开始,每道题会计写代码用时。
CF1661E 怎么感觉像是 ddp,不知道存不存在线性做法,但是 ddp 解决一切。
我秒了。
CF1398F 首先对一个固定的 是非常好求答案的,直接贪心填即可。具体而言,从 开始,如果能填 个同样的就填,否则 跳到下一个不同的位置继续这个步骤。 思考对于 如何加速这个过程。 不怎么优秀的地方在于花了大量时间去寻找下一个能连续填 个相同元素的位置。于是去预处理 表示从 开始不同时存在 0/1
的最长长度,那么答案的形式长成 ,其中 。于是线段树二分即可,似乎常数有点大了,但是这确实是 的。
CF1389G 感觉好困难的题,咋下手啊,这每一步都很有逻辑,只能说被教育了。 先把问题简单化,把边双缩掉,因为边双内的所有点一定可以通过定向互相到达,这样不会有代价产生,所以这个问题放到了树上。 考察关键点这个限制怎么去掉,注意到关键点的含义,它们是需要作为起点的,首先强制定一个关键点为根,然后对于一个内部没有关键点的子树,可以从最近的关键点一直定向过来,所以这些点和关键点的行为平行,可以直接缩在一起,于是现在树的形态就变成了只有叶子节点上有关键点,更确切的说,所以叶子节点都是关键点。 下一步是考察饱和点 如何确定,在上述条件下,饱和点一定形成一个树上的连通块,且连通块内的边都是双向边,连通块外的边都定向为指向这个连通块的有向边。 然后这个可以换根 dp
了。
CF1380F 很简单啊,为啥调了这么久呢。 考虑再没有修改的情况下,直接 dp
即可,定义 表示以 结尾时整个字符串分开的方案数。于是有
于是这个转移只和前两项以及当前的 有关,可以轻松用 ddp
维护,线段树上矩乘很套路。
CF1373F 应该好好反省了,我是做过 JOI 2022 蚂蚁与方糖 那道题的,这道没弄出来确实是弱智。 首先拆环成链,这一步不多说,然后考虑霍尔定理推论,想着去枚举城市集合,其实不用,直接等价为每个连续区间里面的城市均能被满足,即是
这个直接前后缀极值维护一下就行了。
CF1366F 这题挺好的,首先观察答案路径长啥样,抛开走的次数有限制这个条件,考虑可以无限走,那么最优策略就是从 开始,经过一些边,来到某条边 ,然后之后就一直在 这条边上左右横跳。
注意到 ,记 表示走了 条边走到了 时的最大距离,这样是可以 去求的,具体而言,重复 轮,每次枚举走的边 即可求得。
下面解决当步数大于 时的情况,很明显,我们可以通过 步到达这个图上的每一条边,这样就可以开始反复横跳,这个形式很简单,是一次函数,具体而言,假设我们花了 轮,走到了第 条边,之后开始反复横跳,那么对于远处的一个步数 ,走过的路径长度为 ,为了使形式更容易被处理,写成 ,很明显,对于每一条边 , 最大时最优,那么对于 记截距 为 ,在 时的贡献为 ,似乎需要上凸包去求。
其实到上面就可以做了,但是用凸包麻烦了,更容易的做法是直接对于每个一次函数求出它的控制区间,这里控制区间定义为 。具体实现时直接枚举其他的所有一次函数,找出交点把控制区间夹逼出来。过程需要二分,于是总的时间复杂度为 。
题目比较简单,过程十分精妙。
CF1366G 除了数据范围很糟糕以外,这题思路挺好。 首先有一个很显然的状态 表示用 匹配到了 需要的最小步数,于是有
其中 表示从 开始只用执行 可以一直清空到 的最近的位置。 注意到这题不要去考虑删除 这个区间需要的代价,因为这样去做一定会带 ,而上面做法已经把所有情况包含进去了,正确性来自于 是最近的一个满足要求的位置。
总之要学会使用原子状态 去拼整个问题状态。
CF1312G 大概是全网唯一解。 这个转移的式子非常明显,记 表示走到字典树上 节点需要的最小步数,于是
其中 表示遍历到 时出现了多少个 中的点。 这个可以直接上优先队列懒惰删除解决。复杂度 实现难度和思维难度远低于其他所有解法。
GYM103202J 感觉很容易啊,考虑主席树合并,以每个权值为版本建立主席树,初始的时候 是满的,其他树是空的,区间内 赋值成 就直接把版本 的主席树合并到版本 的主席树上。 区间最值可以套个二分,所以合并时要注意保留 版本里面的点不要删,总的复杂度 。
CF1359F 我需要反应更快一点,首先二分答案记为 ,然后对于一对车 如果要在 之内相撞,那么应当满足 ,其中 分别是 到达这两条线交点的时间。 现在问题是如何更快地去 check
,也就是说我需要很快地判断这 条线段有没有交,发现似乎很麻烦。考虑去维护局面上当前存在的线段,先拆开,将一条线段分成两个端点,变成加入和删除两次操作,按纵坐标和斜率排序,每次在删除和加入时直接检查上下的线段是否与它有交即可。过程用 set
维护。
CF1354F 观察到 似乎很有搞头,从最后的局面入手,一定是会剩下 张牌,记为 ,至少会有 那么其他的牌如何造成额外的 贡献呢? 然后我开始贪,想让 造成尽量多的贡献,因为和牌数有关,并且需要时刻满足不多于 张牌,所以 前面的系数最多是 ,这里就有个很抽象的东西,最后一张牌怎么弄,它肯定是最后被加进来然后不删掉,产生的贡献很特殊,所以先考虑把它 ban 掉,用 的复杂度枚举解决,于是剩下 张牌,我要选 记为 ,造成贡献 ,因为 前面没有系数,所以把 集合中的牌按 从小到大往里面扔即可。然后再选出一个 (实际上不是选的而是固定的),里面的牌造成 的贡献。 所以直接先按 排序,定义 表示前面 个数选出来了 个在 中,就可以直接做了,注意一开始要 ban 掉一个。
CF1354G 感觉很有意思,像是小学学的找次品问题。但是注意到装有礼物的盒子不保证重量相等 。 先考虑一个步数很多的做法,首先随便分出来两个集合,然后现在的目标是先找出来任意一对石头,因为石头的大小是固定的,并且是最大的,应该可以比较好地去入手。然后发现 ,这意味着啥,这意味着我每次随机,都有至少 的概率找出来是石头,每次更新最重的,如果跑 次那么错误的概率就只有 了,然后想啊想,我不会这个。 点开题解,第一篇就是马大师的,Orz。抱歉我完全没看懂。。。
正解很聪明了,每次都拿一个已知的全是石头的集合和另外一个大小一样的集合比较,如果不相等,则说明有礼物,我一开始想过,但是没想到可以倍增 跳。 这样我最多跳 20 次就能找到第一个礼物,然后剩下的 30 次匀给找石头。
CF1334F 假设当前处理到了 ,并且 这个数能和 匹配,那么要让它匹配就得去枚举上一个和 匹配上的 。代价是对于 之间的数而言的。即是要删掉所有大于 的数产生的贡献,然后对于小于等于 的数,负的就直接加上。 假设这个贡献为 ,于是式子就写成 。 目标是解决算 ,感觉形式不优美,需要二维数点,考虑再多去发现一些性质。
假设我从 往后找( 匹配 ),找到 都能匹配 ,钦定 ,寻找性质。我想放个图在这里但是我懒了 233。 首先, 后面的部分对于 来说,都是一致的,贡献都是大于 的数加上所有小于等于 的负数。前面分成两个部分考虑,分别是 。 然后这里记两种区间贡献 和 则有
因为 是具有可拆性质的,然后我拆。
抵一下,剩下的
于是这就很舒服了啊,和 位置没关系了,选择 还是 保留直接比即可。然后这是可以做到线性的。
CF1327F 注意 ,看看能不能直接去枚举每一位,关键在于我要去刻画一段区间的与值恰好等于 。 也就是说,对于 所有为 的位置上, 之间里面的所有数这些位置全为 ,而剩下的位置上不能全为 。那么拆位做个前缀和,记 表示 这一位上为 到 位置的前缀和。 然后来描述一个限制 ,应该为
于是只要能算出每一位上合法序列的个数,那么乘起来就是答案。
然后我就开始乱想
现在的问题就是填 序列,要求某些段全填或者某些段不能全填。首先把所有需要全部填 的序列扣出来,这部分答案是固定的。只考虑剩下的不能全填的区间,因为有些位置被强制填了 ,所以不能去用区间长度刻画问题,要用还没有被填的位置个数刻画,为了简化问题,应该把完整包含其他区间的大区间去掉,这样问题是等价的。 首先对于每一个右端点,如果区间限制是 ,只需要考虑最近的左端点即可,否则是考虑最远的左端点,这样就保证了转移的确定性,
然后网上题解到这一步就结束了??!!为啥我不会啊?
哈哈,被薄纱了,这个确实能做了。
定义 表示前面 位最后一个 放在 上的方案数,动动脑子,把对于 需要放 的最后一个位置跑出来,于是得到了
你发现这个可以双指针不用写 数据结构,复杂度就对了。
CF1891E 按相邻数的 gcd
分段,具体是,当 和 的 gcd
不是 的时候就新分一段,发现有 的情况很麻烦,所以把 也分开。 于是操作中间一个就修改了相邻的两个 gcd
,贪心就好了。
CF1260F 搞我心态啊。做了整整一个上午。 考虑把颜色区间扔到数轴上然后扫描线,当前扫到的区间代表的点的集合就是一组合法的。 所以每次只需要考虑加入或者删除一个点答案的变化即可。
我直接开始了!考虑往一个集合大小为 的点集里面加入点 ,并定义 。
然后后面那个是树剖经典题。代码抽象。放个链接 。
CF1313D 被 Kidulthood 嘲讽了,嘤嘤嘤。 这个题就一句话,扫描线只状压维护最后 个线段的交。
CF1379F2 第一步把每个 的小格子拼在一起我会,因为必须放 个,所以每个这样的小格子都要放一个。 然后下面的文字可以在所有题解里看到相同部分。 定义一个左上角不能填的四方格为 ,一个右下角不能填的四方格为 。整个图不合法的条件是存在 在 的左上角。 这个东西用线段树维护每一行 的最小纵坐标, 的最大横坐标。代码小清新。
CF1396C 首先明确,回退只会回退一层,然后状态是关键,需要去刻画的是当前层的 boss 还需不需要被打一下。 先找出所有打怪的策略
用 次 先把小怪突突掉,然后打 boss 一次,遁入下一层
用 次 把所有怪打一下,这是小怪全 gg 了,boss 剩一滴血
用 次 把小怪突突掉,然后用一次 狙死 boss
前两种会让 boss 留一滴血,这时需要再回来用一次 。于是状态定义为 表示对于第 层扫了第一次后 boss 剩多少血。转移显然。