十月杂记
CF
构造
CF2154D *1900
想到的:注意到操作二的作用在于限制猫咪能走的边。但是注意到有一个问题那就是不能出现两个相邻的第二条指令,也就是说每次移动前最多只能删除一个点,这是个问题。那么也就是说在猫移动一次后把它接下来可能移动到的点删除。操作次数的限制很松,所以可以直接任意删一个当前不可能走到的且不在路径上的点删除就行了。不行,这样不行。重新更换思路,注意到整个过程类似一个 bfs,每次寻找可能会走到的最深的点,将这个点 push 入队。如果没有点要 push 说明已经走到了树最深的点。不 bfs 了,观察发现这题和奇偶最短路有点关联,即钦定节点 \(0\) 为根,然后树上拓扑删点,对于每一个节点到它的距离,如果和此时走过的步数奇偶性是相同的,则进行 \(1\) 操作,否则删掉该点。
数论
CF2065G *1700
想到的:观察发现,\(a_i\) 与 \(a_j\) 的乘积中必然只能包含小于等于两个不同的质因数。此时分讨一下:
- 如果只包含一个,那么 \(a_i\) 与 \(a_j\) 中任意一个数的质因数个数不能超过 \(2\) 并且必有一个等于 \(2\)。
- 如果包含两个,那么两个数的质因数的指数只能为 \(1\) 且两个数包含两个不同的质因数。
然后组合一下应该就行了。
贪心
CF2162E *1600
想到的:考虑进行一次操作会有怎样的后果。定义一个数的贡献 \(f_i\) 表示以 \(i\) 结尾的回文子串的个数。容易贪心的考虑,如果前面的数中有未出现过的,那么肯定放这个是最优的,因为它不会产生任何贡献。对于前面已经出现过的数,新添加的 \(a_i\) 都有可能和其匹配然后变成回文,所以能否用 vector<vector<int>> pos(n) 记录每一个数的下标然后贪心的找满足 \(pos_{x,j}+1\sim i-1\) 是回文的最少的数,然后添加?发现往后添加数的过程中产生的回文数量取决与之前的数所可能产生的回文,所以不妨每次都找一个 \(x\) 使得 \(lst_x\) 最小,然后把 \(x\) 添加到末尾,更新 \(lst_x\)。其中 \(lst_x\) 表示 \(x\) 最后出现的位置。
666,结论对了。
CF1801B && P13532 *1800
想到的:应该是 dp 了。主要是因为每次必须买一个礼物,所以是由后效性的,不能贪心。注意到最后必然有一个人取到 \(a\) 和 \(b\) 中的最大值,所以接下来该怎么办?不对,不一定是全部的最大值,而是取到的最大值。此时便有 \(n^2\) 做法了。即枚举每一个 \(a_i\) 是否成为第一个人的最大值,然后把所有的满足 \(a_j\le a_i\) 和 \(b_j\) 取一个与 \(a_i\) 最接近的数即可。如何优化?可以排序然后搞一下吗?不用 dp 啊,这不是排序加二分吗?先搞出每一个 \(a_j\ge a_i\) 的最大 \(b_j\) 然后再按 \(b\) 排序,二分一下就行了。不用按 \(b\) 排序,只需要用一个 multiset 维护一下,同时记录一下当前的最大 \(b_i\) 然后如果 \(maxn>a_i\) 就直接算贡献,否则在 multiset 中二分即可。
细节较多,洛谷数据好水啊!
CF2151C *1400
想到的:反悔贪心吗?贪心的考虑,每次都积攒够 \(k\) 个人再释放人。即 \(ans = \sum_{i=1}^{n} a_{i+k}-a_i\)。但可能会出现下面这种情况,就比如:
式子有一定问题,但不影响理解。或者换一种角度想,题目就是要求两两配对 \(a_i\) 使得差值最大,同时这个配对有一定的依赖性,具体来说就是不能连续选 \(\ge k\) 个同样的匹配。哦,转换成染色,就是指黑白染色然后不能有\(\ge k\) 个 \(a_i\) 染相同的颜色,求黑色与白色的差值的最大值,即令黑色最小。
能 dp 吗?但有 \(n\) 次询问,怎么办呢?第一个询问是好处理的,现在看怎样 \(O(1)\) 转移询问。考虑每次询问本质,即为将一些点颜色互换。完了,成废物了。贪心的考虑可以把最小的黑色点权找出,然后优化一下便 ok 了。
CF1054D *1900
想到的:考虑异或运算的本质,即为当该位出现的 \(1\) 的个数为奇数时便不为 \(0\)。能不能 01 trie 呢?贪心的考虑,每次能否令最高位 \(k\) 都变成 \(0\),然后留几个 \(1\) 下来?套路的,考虑枚举每个子串的右端点,然后进行一些判断。感觉比较想,因为可以开一个桶记录前 \(i\) 个数的第 \(k\) 位的 \(1\) 的个数,然后枚举到 \(j\) 的第 \(k'\) 位时只要判断前面有多少个 \(i'\) 满足奇偶性相同即可。但怎样去重以及处理翻转操作呢?
考虑将区间异或为 \(0\) 数量最小化。即区间内所有数位上的 \(1\) 的个数都为偶数。通过前缀和转换,变成让每个前缀相同的数最少。于是大胆猜结论,每次都去较少的。
没想到的:又是正难则反!
ds
CF2138B *1900
想到的:注意到仅使用操作一来达到目的的操作次数其实等同于 \(l\sim r\) 之间逆序对的个数,所以如果使用了操作二后能够使操作次数更少仅可能是使逆序对个数一次减少一个以上。手玩发现,只要出现形如 \(a\cdots b\cdots c\) 且满足 \(a>b>c\) 的子序列,答案就不会是 YES。怎么维护这个东西呢?考虑先用单调栈预处理出对于每一个 \(b\) 的 \(\min\{c - a + 1\}\)。然后把这些区间丢到一个 vector 里面去按左端点排序,接着用 \(\operatorname{ST}\) 表去维护区间最小右端点。每次查询时就是先二分出哪一段区间满足 \(l\le a\le r\),接着在 \(\operatorname{ST}\) 表中查询最小 \(c\),如果有 \(c\le r\),那么显然满足上述子序列。
dp
CF2167F *?
CSP 考前最后一天写的题,带给我 RP 吧!
想到的:应该就是树形换根 dp 了。先考虑求出以 \(1\) 为根的树的答案。从 \(1\) 开始 dfs,然后判断一下 dfs 到的点的子树大小 \(sz\) 是否满足 \(sz\ge k\),如果满足,就说明可以产生贡献。现在考虑如何计算以其它点为根的答案。似乎都不用 dp,直接先钦定 \(1\) 为根,然后 dfs 一遍算出每一个点的 \(sz\)。接下来直接拆贡献,考虑每一个点的父亲为谁,然后算一下剩下的节点数量是否 \(\ge k\)。注意每一个点都可以为根。
草,看错题了。哦,只需要再乘上有多少个点可以为根即可。
CF2153D *1800~~~~
想到的:套路的考虑,断环为链,然后考虑一些性质。发现操作一定会把一些连续的数变得相同,然后注意到把连续四个数变得相同一定不会优于把连续三个数或两个数变得相同。在此时考虑线性 dp,转移只有两种可能,即从前两个或前三个转移而来。设 \(f_i\) 表示考虑完前 \(i\) 个的最小花费,则转移为:
答案有三种可能,即第一个数可以从倒数第二个、倒数第一个和它本身转移而来,取 \(\min\) 即可。不对,需要二维,记录一个从哪里转移而来的即可。
不对,答案的处理略显麻烦。怎么搞呢?只有四种情况,都跑一遍即可,大力分讨。
没想到的:可以把分讨过程转换成简单的循环,然后每次让 \(a\) 向后移一位。
CF2138C1 *1800
想到的:感觉是让每一层都放相同的,然后如果放不了了就停止,输出答案。但为什么 tag 是背包 dp?个人感觉一个比较对的结论是在前面几层都放相同的,知道不能放或到达较浅的层数后就停止。此时,就会出现每一层究竟使用 \(0\) 还是 \(1\) 的问题。这涉及到剩余的 \(0\) 和 \(1\) 的数量,所以类似一个背包 dp。
状态定义为 \(f_{i, j}\) 表示 dp 完前 \(i\) 层,还剩 \(j\) 个 \(0\) 的可行性,\(sz_i\) 表示第 \(i\) 层的节点个数。
转移为:\(f_{i,j}=\operatorname{OR}f_{i-1,j},f_{i-1, j - sz_i}\)
Luogu
P10842 \(\textcolor{green}{绿}\)
想到的:看到的换根 dp。考虑一个点为根时会和那些链产生贡献,对于不同的贡献,记录一下链的个数。状态的设计是何?\(f_u\) 表示以 \(u\) 为根的距离之和,\(g_u\) 表示在以 \(u\) 为根的子树中有多少条链。
第一遍 dfs 的转移如下:
第二遍 dfs(即换根)的转移如下:
AT
abc
427e *?
想到的:套路的,考虑把所有垃圾的移动转换成高桥的移动。然后这样就只需要将原来的矩阵扩大 9 倍,然后去 bfs 即可。司马了,还是得正向 bfs,因为具有后效性。
427f *1600
想到的:打表发现当 \(n=30\) 时的状态数大约为 \(2\times 10^6\),所以考虑 meet in the middle。用四个 map 存一下前一半的状态和后一半的状态,然后处理一下前一半状态的最后一个数和后一半状态的第一个数的情况即可。需要卡一下常,用 unordered_map 过了。
arc
206b *1100
想到的:应该要往逆序对的方向上去想,考虑一个数要往前移,那么必然会和所有在它前面的并且大于它的数有互换操作(在这里我们假定每次都从小到大把数摆好,这样就避免讨论往后操作的情况)。然后就可以这样建图,然后考虑把所有边都删掉,求最少删掉的点数。可是为什么 tag 是二分?现在有一个线段树做法,但后面删点的部分怎么搞。可是这样建出来的图的边数是 \(n^2\) 的级别的啊!这样似乎不是很行。考虑一个数在图里的度数为前面比它大的数的个数以及后面比它小的数的个数,然后从大到小贪心似乎就有正确性了。怎样判断图完全不连通了?权值线段树!势能分析复杂度为 \(O(n\log n)\),应该是对的吧……第二部分假了……这是一个二位偏序!666,最长上升子序列是什么鬼。
没想到的:一个显然的结论,只需要令每一个颜色的史莱姆都递增即可。然后因为重新染色后不会与其它任何一种颜色有冲突,所以可以不在考虑。接着又是正难则反!!!因为对于每一个颜色要改变最少一些数使剩下的为上升序列所以考虑留下最多的。于是变成了最长上升子序列……【我是傻逼】
206c *1500
想到的:容易注意到构造出来的数组中必然不出现两个及以上的 \(a_i=i\)。连续两个数之中必然存在一个 \(a_i \in \{i-1, i+1\}\),并且如果 \(a_i\notin \{i-1,i+1\}\),则必然 \(a_{i+1}=a_{i-1}=i\)。Moreover,\(|a_i-i|\le 2\),且如果出现了一个 \(|a_i-i|=2\),则其它的数都必然被确定了。如果全是 -1,则每个数都可以 \(|a_i-i|=1 \operatorname{OR} 2\)。那如果有 -1 呢?先判断一下是否无解,然后进行一些计算。之前的 \(|a_i-i|\le 2\) 是假的,但其它的都是对的。
接着就是考虑形如 RRRRXLLLL 的构造了。但还是脑残了,总是差一点……
agc
010f
想到的:观察到如果一个节点的点权比其周围点权的和都要小或相等,那么这个点必然就会成为必败点,因为先手走不出去。所以先枚举每一个点是否可能成为必胜点,然后 dfs 去递归的判断其周围的点是否全是比败点或它自己就是必败点,时间复杂度 \(O(n^2)\)。哦,需要可行性 dp,典的。有一个问题,只有如果一个节点的点权比其周围点权都要小或相等才行。
成弱智了,刚刚的发现没错,所以说每一个人便只能像比它小的点走,然后每次状态都会翻转,dfs 即可。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using ull = unsigned long long;
using i128 = __int128;
using PII = pair<int, int>;
using PLL = pair<ll, ll>;
constexpr ll inf = (1ll << 62);
constexpr int N = 3000;
int n;
vector<int> a(N), b(N);
vector<vector<int>> G(N);
bool dfs(int u, int fa, int step) {
bool ok = false;
for (auto v : G[u]) {
if (v == fa) continue;
if (b[v] < b[u]) {
bool flag = dfs(v, u, step ^ 1);
ok |= (flag ^ 1);
}
}
return ok;
}
void solve() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
u--;
v--;
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
b[j] = a[j];
}
if (dfs(i, -1, 1)) {
cout << i + 1 << " \n"[i == n - 1];
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
::::
模拟赛
s
提高级
27
a
思路:双端队列模拟即可。
b
思路:并查集判连通性,然后模拟。
c
思路:暴力 dp 题,似乎并不能优化,于是 \(O(nk)\) 过了。
d
思路:设 \(dis_{i,j}\) 表示从 \(i\) 到 \(j\) 的最短路长度。对于每一个 \(i\),如果上面的奶牛前往 \(j\) 吃草,那么必须满足如下不等式(\(val_i\) 表示 \(i\) 上草的价值):
简单移项得到:
于是先正常跑一遍 \(\operatorname{dijkstra}\),然后建分层图,第二层表示吃了草后的最短路长度,然后对于每一个草加一个点权即可。
28
傻逼模拟赛,谁爱补谁补去吧。
初二级
2
a
思路:模拟题。
b
思路:奇偶最短路跑一下就行了。
c
思路:注意到同一天买进的物品可以同一天卖出,所以可以转换成在今天早上买了,然后在晚上卖掉。于是便可以把每个物品的价值赋值成相邻两个价值的差,这样在跑一边完全背包就可以搞定了。主要还是需要发现可以这样转换。
d
思路:注意到重新定义本质不同。定义 \(f_u\) 表示以节点 \(u\) 结尾的合法括号串的个数,一眼得到方程为:
其中 \(v\) 表示上一个与 \(u\) 匹配的字符。最后树上前缀和统计一遍即可。
noip
7
a
这是要我现场学高斯消元吗?
补了一下高斯消元,然后直接暴力枚举前 \(n+2\) 个然后排除掉枚举的那个数解方程,接着判断第 \(n+3\) 个数是否符合,如果是就直接输出枚举的那个数。如果全部都枚举完了,那就输出第 \(n+3\) 个数。
b
板子,不予评论。
c
只会暴力……
怎么是堆加可持久化线段树大法好!观察到 \(k\) 的范围很小,所以可以考虑每次都找到原集合中的最大值,然后将这个最大值删掉,如此重复 \(k\) 次即可。现在考虑如何维护该最大值,套路的,考虑维护以每个位置作为左端点连续子串的最大值,但是如何推广到每一个位置上呢?可以使用一个 \(nxt_i\) 表示 \(a_i\) 在 \(i\) 后出现的第一个位置,然后观察到,每次往后移动一位就相当于把 \((i,nxt_i)\) 这段区间的贡献都减掉 \(a_i\),并且把 \(a_i\) 变为无穷小,于是考虑可持久化线段树维护。由于需要区间修改,所以要标记永久化。于是拿个大根堆记录。
8
a
一眼状压,然后怎么让我 \(4^n\) 过了。还得是跑不满……
b
确乎是 dsu on tree 了。套路的,考虑维护每个节点到根节点的距离长度 \(val_u\) 以及每个点的深度,然后距离为 \(k\) 的点对便可以直接判 \(val_u+val_v-2*val_{root}\) 是否等于 \(k\) 即可。接着一个全局的 map 表示在值为 \(x\) 的情况下最小的节点的深度的编号。跑完所有轻子节点的答案后就 map.clear(),重子节点则直接继承,最后统计该子树的答案即可。放个代码吧,想和写花了两个半小时。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using ull = unsigned long long;
using i128 = __int128;
using PII = pair<int, int>;
using PLL = pair<ll, ll>;
constexpr ll inf = (1ll << 62);
constexpr int N = 2e5 + 10;
int n, k;
ll cnt = 0;
vector<int> depth(N), sz(N), son(N, -1), ans(N, INT_MAX);
vector<ll> val(N);
vector<vector<PII>> G(N);
map<ll, int> tot;
void dfs1(int u, int fa) {
sz[u] = 1;
depth[u] = (fa == -1 ? 0 : depth[fa]) + 1;
for (auto [v, w] : G[u]) {
if (v == fa) continue;
val[v] = val[u] + w;
dfs1(v, u);
sz[u] += sz[v];
if (son[u] == -1 || sz[son[u]] < sz[v]) {
son[u] = v;
}
}
}
int calc(int u, int fa, int root) {
int res = INT_MAX;
if (tot.count(k + 2 * val[root] - val[u])) {
res = min(res, depth[tot[k + 2 * val[root] - val[u]]] + depth[u] - 2 * depth[root]);
}
for (auto [v, w] : G[u]) {
if (v == fa) continue;
res = min(res, calc(v, u, root));
}
return res;
}
void add(int u, int fa) {
if (!tot.count(val[u])) tot[val[u]] = u;
else if (depth[tot[val[u]]] > depth[u]) tot[val[u]] = u;
for (auto [v, w] : G[u]) {
if (v == fa) continue;
add(v, u);
}
}
void dfs2(int u, int fa) {
int res = INT_MAX;
for (auto [v, w] : G[u]) {
if (v == fa || v == son[u]) continue;
dfs2(v, u);
tot.clear();
res = min(res, ans[v]);
}
if (son[u] != -1) {
dfs2(son[u], u);
res = min(res, ans[son[u]]);
}
if (!tot.count(val[u])) tot[val[u]] = u;
else if (depth[tot[val[u]]] > depth[u]) tot[val[u]] = u;
if (tot.count(k + 2 * val[u] - val[u])) {
res = min(res, depth[tot[k + 2 * val[u] - val[u]]] + depth[u] - 2 * depth[u]);
}
for (auto [v, w] : G[u]) {
if (v == fa || v == son[u]) continue;
res = min(res, calc(v, u, u));
add(v, u);
}
ans[u] = res;
}
void solve() {
cin >> n >> k;
for (int i = 1; i < n; i++) {
int u, v, w;
cin >> u >> v >> w;
G[u].push_back({v, w});
G[v].push_back({u, w});
}
dfs1(0, -1);
dfs2(0, -1);
int res = INT_MAX;
for (int i = 0; i < n; i++) {
res = min(res, ans[i]);
// cout << ans[i] << " \n"[i == n - 1];
}
if (res == INT_MAX) {
cout << "-1\n";
} else {
cout << res << "\n";
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
::::
c
瞪出来答案具有单调性,所以可以直接二分答案,check 是好实现的,只需要哈希加个 map 就行了。
9
a
思路:怒了,耗费了一个小时,贪心地考虑,让除了第二个数的其它数都相乘,然后判断能否整除第二个数即可。
b
思路:正解上下界网络流,但乱搞可过。每次找到 DAG 中的最长链,然后将其删除,看可以删除几次。
c
思路:注意到若:
则:
所以,只需要维护每个质因数出现的个数即可。
10
a
思路:水题。
b
思路:把所有的 Baka 数打表打出来并且去掉倍数关系后发现只有 \(466\) 个。同时注意到第七个数就已经超过 \(1000\) 了,所以可以前六个数容斥,后面的数直接类似调和级数暴力碾过去就 ok 了。细节是调和级数那里筛出来的数不能被前六个整除。
c
思路:旋转卡壳题,不予评论。
12
a
思路:见到异或想到的第一眼便是 \(01\operatorname{Trie}\) 拆位,但是在经过思考以后我们发现并不可行,不过拆位这个思想或许仍然用的上。如果不用管 \(x\) 可以怎么做?考虑一个比较典的贪心,即从高到低去枚举每一个数的二进制位,尽可能的满足该位为 \(1\),然后可以得到一个满足该条件的数的集合 \(S\),下一位的枚举只需在 \(S\) 找满足的数即可。但是怎样把这个 \(S\) 找出来呢?观察到如果 \(a\) 中的一堆数的二进制中的某一位相同,则这堆数一定在同一个区间内,即 \([0, 2^k-1]\) 或者 \([2^k, 2^{k+1}-1]\)。于是我们只需要判断 \([l,r]\) 中有没有数在这段区间中就行了。如果没有就说明这一位只能是 \(0\)。
发现这是好维护的,因为 \(a_i\le 10^5\),所以直接上主席树就行了。那如果把 \(x\) 考虑在内呢?简单的,只需要把查询时的 \([0, 2^k-1]\) 或者 \([2^k, 2^{k+1}-1]\) 改成 \([\max(0, 0 - x), \max(0,2^k-1 - x)]\) 或者 \([\max(0, 2^k), \max(0, 2^{k+1}-1)]\) 就行了。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;
using ull = unsigned long long;
using i128 = __int128;
using PII = pair<int, int>;
using PLL = pair<ll, ll>;
constexpr int N = 3e5 + 10;
int n, m, tot;
vector<int> a(N), root(N);
struct Node {
int l, r, val;
} tree[N << 5];
void pushup(int p) {
tree[p].val = tree[tree[p].l].val + tree[tree[p].r].val;
}
int build(int l, int r, int k, int p) {
tree[++tot] = tree[p];
p = tot;
if (l == r) {
tree[p].val++;
return p;
}
int mid = l + r >> 1;
if (k <= mid) tree[p].l = build(l, mid, k, tree[p].l);
else tree[p].r = build(mid + 1, r, k, tree[p].r);
pushup(p);
return p;
}
int query(int l, int r, int x, int y, int p1, int p2) {
if (x <= l && r <= y) return tree[p2].val - tree[p1].val;
int mid = l + r >> 1, ans = 0;
if (x <= mid) ans += query(l, mid, x, y, tree[p1].l, tree[p2].l);
if (mid < y) ans += query(mid + 1, r, x, y, tree[p1].r, tree[p2].r);
return ans;
}
void solve() {
tree[0] = {0, 0, 0};
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
root[i] = build(0, 3e5, a[i], root[i - 1]);
}
while (m--) {
int b, x, l, r;
cin >> b >> x >> l >> r;
int ans = 0, cnt = 0;
for (int i = 17; i >= 0; i--) {
if (!((1 << i) & b)) {
int xx = max(0, cnt + (1 << i) - x), yy = max(0, cnt + (1 << i + 1) - 1 - x);
int num = query(0, 3e5, xx, yy, root[l - 1], root[r]);
if (num) {
cnt += 1 << i;
ans += 1 << i;
}
} else {
int xx = max(0, cnt - x), yy = max(0, cnt + (1 << i) - 1 - x);
int num = query(0, 3e5, xx, yy, root[l - 1], root[r]);
if (num) {
ans += 1 << i;
} else {
cnt += 1 << i;
}
}
}
cout << ans << "\n";
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
::::
b
思路:神秘树形 dp。考虑在 dfs 过程中枚举一个叶子节点的祖先的状态,然后从下往上类似背包的合并。由主定理,这东西不会 TLE……
c
思路:树链剖分,还没补。
14
a
思路:拼好题,树的直径加个单调队列就行了。

浙公网安备 33010602011771号