2024.12 做题记录
这个月可能没做啥特别大的思维题。但学的东西比较多。基本没记录网络流作业里面的那些题。太水的且没有任何启发性的我也略过了。
网络流
网络流问题是一类解决从源点 \(s\) 到汇点 \(t\),边有容量限制的情况下使得流最大的一类问题。
最基本的思想是一类贪心,考虑从源点 \(s\) 到汇点 \(t\) 任意选择一条路径,使得流的最小值非 \(0\),也就是找到一条合法增广路。然后减去增广路流量,继续这个过程,直到找不到合法增广路为止。但很不幸这个贪心是假的。
我们考虑给贪心加上反悔操作。当我们减去一条边的流量时,给它的反边加上这个流量,也就是我们允许退流操作。这样再跑这种增广路算法,正确性就有保证了,这种方法统称为 Ford-Fulkerson 算法,而这种单路增广的算法被称为 Edmond-Karp (EK) 算法。但这种方法的效率并不高。
于是我们考虑我们能不能在正确性有保证的情况下进行 多路增广。怎样增广其实非常显然,我们只要阻止这一次增广造成的退流即可,于是我们考虑一个有向无环图。图上最经典的 DAG 图应该就是最短路图了,于是我们考虑每次增广完都跑最短路建出最短路图,在这个图上用 DFS 跑多路增广即可。这就是著名的 Dinic 算法。
当然还需要一个保证复杂度的优化,当然我也不知道为什么能优化。就是 DFS 的时候每条边最多流一次,流完下次再来就不流了。就是对每个点记个 cur 数组就行了。这就是当前弧优化。总时间复杂度为 \(O(n^2m)\),但我不会证,但其实随意了。
001. P3376 【模板】网络最大流 - Dinic 版本
难度:提高+ / 省选-
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MAXN = 205;
const LL MAXM = 10005;
const LL INF = 1e18 + 5;
LL n, m, s, t, tot = 1, V[MAXM], W[MAXM], head[MAXN], nxt[MAXM], cur[MAXN], ans = 0, Dist[MAXN];
void AddEdge(LL u, LL v, LL w)
{
tot ++;
V[tot] = v; W[tot] = w;
nxt[tot] = head[u]; head[u] = tot;
return;
}
bool BFS()
{
for(LL i = 1; i <= n; i ++) Dist[i] = 0;
queue<LL> Q; Q.push(s); Dist[s] = 1;
while(!Q.empty())
{
LL u = Q.front(); Q.pop();
for(LL i = head[u]; i; i = nxt[i])
{
LL v = V[i], w = W[i];
if(!w || Dist[v]) continue;
Q.push(v); Dist[v] = Dist[u] + 1;
if(v == t) return true;
}
}
return false;
}
LL DFS(LL u, LL Lim)
{
if(u == t) return Lim;
LL ress = Lim;
for(LL i = cur[u]; i && ress; i = nxt[i])
{
cur[u] = i;
LL v = V[i], w = W[i];
if(!w || Dist[v] != Dist[u] + 1) continue;
LL k = DFS(v, min(ress, w));
if(!k) Dist[v] = 0;
W[i] -= k; W[i ^ 1] += k;
ress -= k;
}
return Lim - ress;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> s >> t;
for(LL i = 1; i <= m; i ++)
{
LL u, v, w;
cin >> u >> v >> w;
AddEdge(u, v, w); AddEdge(v, u, 0);
}
LL nowflow = 0;
while(BFS())
{
for(LL i = 1; i <= n; i ++) cur[i] = head[i];
while(nowflow = DFS(s, INF)) ans += nowflow;
}
cout << ans << '\n';
return 0;
}
002. *P4311 士兵占领
难度:省选 / NOI-
\(Key \space Observation \space 1\) 正难则反:先把所有格子都放满骑士,我们考虑最多可以去掉多少种骑士。骑士有两种,一种是满足一种贡献的骑士,一种是满足两种贡献的骑士。显然最后只满足一种贡献的骑士是不可忽略的。所以我们 考虑怎么去掉最多的满足两种贡献的骑士。
\(Key \space Observation \space 2\) 网络流建模,最大流建模:我们考虑怎样构造二贡献骑士。考虑如果第 \(i\) 行最多可以贡献多少个可去掉的二贡献骑士,其实就是 这一行格子总数 - 被 Ban 掉的格子总数 - 需要的骑士总数,特殊的,如果这个值小于 \(0\),那么说明构造不出任何方案使得限制成立。列是与行同理的。那么我们考虑由源点 \(s\) 向每一个行编号连一条容量为 \(p\) 的边,\(p\) 代表第 \(i\) 行最多可以贡献多少个可去掉的二贡献骑士个数。同理,我们由每一个列编号连向汇点 \(t\) 一条容量为 \(p\) 的边,定义是同理的。考虑行与列之间的连边,对于一个点 \((i,j)\),如果没被 Ban,那么由行编号为 \(i\) 的点向列编号为 \(j\) 的点连一条容量为 \(1\) 的边,表示可以构造出一个二贡献骑士。
于是答案就是没被 Ban 的格子总数减去最大流。
003. P10930 异象石
难度:省选 / NOI-
维护树上动态路径并的题目。
\(Key \space Observation \space 1\) 既然动态加点,我们动态的考虑问题。考虑加一个点后会对总共的有什么贡献。其实结论我认为不难猜,就是 \(dist(u_1, i)+dist(i,u_2)-dist(u_1,u_2)\),其中 \(u_1\) 代表第一个 dfn 小于 \(u\) 的 dfn 的选择点,\(u_2\) 则是第一个 dfn 大于 \(u\) 的 dfn 的选择点。
考虑简单证明 \(Key \space Observation \space 1\),假设以 \(1\) 为根,分类讨论:
- \(u_1\),\(u_2\) 与 \(i\) 在同一条链上,其实就是根本没有增加贡献,其实就是一加一减,把贡献全消了。
- \(u_1\) 与 \(i\) 在同一条链上,\(u_2\) 不在,那么与第一种情况是同理的,也是把贡献全消除了。
- \(u_2\) 与 \(i\) 在同一条链上,\(u_1\) 不在,与情况二对称。
- \(u_1\),\(u_2\) 与 \(i\) 在不同子树,那么就是容斥,两个贡献然后把算重的两段合成一段删去就是了。
- \(u_1\) 与 \(u_2\) 在同一条链上,\(i\) 不在,这种情况显然不存在。
这个过程可以用 set 维护,其实维护方式我认为没啥难的,注意处理不存在 \(u_1,u_2\) 的情况就行了。
004. P4318 完全平方数
难度:省选 / NOI-
这个做法应该完全没有这个难度。性质题。
\(Key \space Observation \space 1\) 正难则反,考虑是完全平方数正整数倍的数。
\(Key \space Observation \space 2\) 答案满足可二分性,应该二分后判断。
\(Key \space Observation \space 3\) 满足条件的基础是 \(k^2|d\),此时 \(k\) 就很小。且有用的只有质数,先预处理出来然后暴力 DFS 容斥。
\(Key \space Observation \space 4\) 考虑你容斥的过程,因为你都是质数,所以你的 LCM 等价于乘积。乘积的增长速度巨快,完全容斥不满,于是就跑的飞快。
\(Key \space Observation \space 5\) 考虑你验证的质数还是非常多,于是你考虑把质数的平方从大到小排序。每次二分寻找可行上界即可。
005. P4451 [国家集训队] 整数的lqp拆分
难度:省选 / NOI-
\(Key \space Observation \space 1\) 较为显然但重要的一个式子,就是设分拆 \(i\) 的答案为 \(g_i\),那么 \(g_i=\sum f_i \times g_{n-i}\)。
发现这个式子很像卡特兰数的一种递推式子,可以考虑生成函数。但是我不会,于是打表找规律。
\(Key \space Observation \space 2\) 发现,\(f_i=2\times f_{i-1}+f_{i-2}\)。于是你可以矩阵加速递推过掉前面的点。
\(Key \space Observation \space 3\) 注意到矩阵的幂满足数论中的费马小定理。所以边读入边取模,然后就做完了。
二项式反演
就是一个式子:
二项式反演主要用于刻画一类,至少选 \(k\) 个和恰好选 \(k\) 个的问题。具体的,这里的 \(g(i)\) 一般对应至少 \(k\) 个,而 \(f(i)\) 一般对应恰好 \(k\) 个。
006. *P4859 已经没有什么好害怕的了
难度:省选 / NOI-
我们首先把 \(k\) 变为 \(\frac{n+k}{2}\)。
恰好 \(k\) 个的限制过于严格,我们考虑放宽变为至少 \(k\) 个,然后二项式反演求出恰好 \(k\) 个的答案。
设 \(g_i\) 代表至少选 \(i\) 个的方案数,\(f_i\) 代表恰好选 \(i\) 个的方案数。
我们先把 \(a\) 和 \(b\) 都从小到大排序,方便我们求解 \(g_i\)。
\(Key \space Observation \space 1\) 考虑动态规划,定义 \(F_{i,j}\) 代表 \(a\) 数组中的前 \(i\) 个位置,对应了 \(j\) 组的方案数,其他对不对应我不管。
那么有显然的转移,\(F_{i,j}=F_{i-1,j}+F_{i-1,j-1}\times (t_i-(j-1))\),\(t_i\) 代表 \(b\) 数组中有多少个小于等于 \(a_i\) 的数。
然后 \(g_i=F_{n,i} \times (n-i)!\),表示其他 \((n-i)\) 个对随意组合,不管行不行。然后二项式反演求出 \(f_i\) 就做完了。
动态 DP
动态 DP 是一种思想,将 DP 中的简单转移写作矩阵乘法的形式或 \((\max/\min,+)\) 矩阵乘法的形式。然后修改使用线段树维护矩阵即可。
最后所需要的值一般是所有矩阵的乘积,也就是线段树根节点维护的值。
007. [ABC246Ex] 01? Queries
难度:省选 / NOI-
设 \(f_{i,0/1}\) 为以 \(0/1\) 结尾的本质不同子序列数。则有转移:
s[i] = '0'
- \(f_{i,0}=f_{i-1,0}\)
- \(f_{i,1}=f_{i-1,0}+f_{i-1,1}+1\)
s[i] = '1'
- \(f_{i,0}=f_{i-1,0}+f_{i-1,1}+1\)
- \(f_{i,1}=f_{i-1,1}\)
s[i] = '?'
- \(f_{i,0}=f_{i-1,0}\)
- \(f_{i,1}=f_{i-1,0}+f_{i-1,1}+1\)
将初始矩阵设为 \(3\times 1\) 的,分别记录有多少个 \(f_{i,0},f_{i,1}\) 和 \(1\),注意需要记录 \(1\) 是因为转移方程里面有 \(1\)。
三种方程的转移矩阵都是显然的。把它们放到线段树上。然后你要做的就是单点修改即可了。
008. P1453 城市环路
难度:提高+ / 省选-
主要不是想记题咋做。是记一类基环树的判环 Trick。很简单,就是用并查集维护要断的环的其中两个相邻节点。然后强制不选 \(S\) 跑一遍树形 DP,再强制不选 \(T\) 跑一遍树形 DP。跟环装 DP 有异曲同工之妙。
*009. P8867 [NOIP2022] 建造军营
难度:省选 / NOI-
见我的 2022-2023 赛季选讲 PPT
010. P8820 [CSP-S 2022] 数据传输
难度:省选 / NOI-
见我的 2022-2023 赛季选讲 PPT
011. P8820 [CSP-S 2022] 星战
难度:省选 / NOI-
见我的 2022-2023 赛季选讲 PPT
012. P6190 [NOI Online #1 入门组] 魔法
难度:省选 / NOI-
简单题。考虑经典 Trick —— 将图转化为邻接矩阵的形式。设 \(f_{k,i,j}\) 代表使用 \(k\) 次魔法,从 \(i\) 到 \(j\) 的最短路。但是注意到 \(k\) 很大,所以我们考虑用矩阵乘法优化转移。
转移矩阵就是将一条边免费。矩阵应为 \((\min,+)\) 矩阵。
\((\min,+)\) 矩阵乘法
因为 \(\min\) 对 \(+\) 具有分配率。且其转移形式又类似矩阵乘法,所以可以定义广义的矩阵乘法,即 \((\min,+)\) 矩阵乘法。
Matrix friend operator * (Matrix P, Matrix Q)
{
Matrix R; R.Clear();
for(LL k = 1; k <= n; k ++)
for(LL i = 1; i <= n; i ++)
for(LL j = 1; j <= n; j ++)
R.A[i][j] = min(R.A[i][j], P.A[i][k] + Q.A[k][j]);
return R;
}
然后你暴力转移就好了。没啥难的。
013. P3203 [HNOI2010] 弹飞绵羊
难度:省选 / NOI-
\(Key \space Observation \space 1\) 根号平衡题。定义 \(f_i\) 代表从 \(i\) 跳出 \(i\) 所在块所需要的步数。\(t_i\) 表示 \(i\) 跳出所在块所到达的位置。然后对序列按照 \(\sqrt{n}\) 分块,暴力跳,暴力处理。每次询问最多跳 \(\sqrt{n}\) 次。预处理也是 \(O(n\sqrt{n})\)。(弹飞绵羊 Trick)
014. P3396 哈希冲突
难度:提高+ / 省选 -
\(Key \space Observation \space 1\) 如果模数 \(p>\sqrt{n}\),则暴力计算复杂度是正确的。
\(Key \space Observation \space 2\) 如果模数 \(p<\sqrt{n}\),则可以预处理 \(f_{i,j}\) 表示模数为 \(i\),余数为 \(j\) 的权值和,然后直接查就可以了。
*015. P1989 无向图三元环计数
难度:提高+ / 省选-
很难想到。图上三元环计数板子题。
三元环计数
考虑给每条边定向。度数小的向度数大的连边。你统计的就是 \(u\space to\space v\) 且 \(v\space to\space w\) 且 \(u\space to\space w\) 的个数。
这样你直接暴力枚举复杂度就是正确的。
首先不难发现这个图是一个有向无环图。但其实也挺难发现。比如原图就是一个三元环,那么连出来的图也是三元环。为了保证原图的偏序性质,如果度数相等,那么比较编号大小即可。
接下来证明:
- 如果 \(u\) 点在原图上的度 \(\leq \sqrt{m}\),那复杂度显然是对的。
- 反之,那么由于边只有 \(m\) 条,所以它往出连的点最多只有 \(\sqrt{m}\) 个。然后复杂度依然是对的。然后做完了。
016. HH 的项链
难度:提高+ / 省选-
Bonus:这题用莫队做会简单到爆,但是貌似被卡了?不知道,没试过。
这题挺典的。考虑离线扫描线,把询问挂右端点上。
思考:一个点有贡献意味着什么?答案是与这个点同色的最靠近他的左边的点小于询问区间左端点。
这是好维护的,我们记每个位置的 \(las\) 即可。每次加入一个新点,把其 \(las\) 贡献消去,加上自己的贡献。然后回答当前节点为右端点的全部询问,就是区间 \([L,R]\) 有贡献的点数。这个用树状数组维护即可。
Bonus2:这题相当于把贡献挂自己身上,还有一个题是把询问挂在自己 \(las\) 身上。总之怎么好做怎么做。
2 - SAT 问题
挺精妙的一类问题。巧妙地将变量间的适应性问题转化为了图论相关问题。2 - SAT 一般有两种条件。一个是与,也就是合取,那么对于这类条件我们只需要给相应的变量直接赋值就可以了。而真正的难点在于或,也就是析取,那么我们考虑建图连边。
边的意思:如果 \(u\) 向 \(v\) 连了一条有向边,那么条件为 若 \(u\) 则 \(v\)。又考虑到每个点有两种取值,那么我们可以考虑显然的拆点,即 \(u\) 点代表 \(u\) 取真的情况。而 \(u+n\) 则代表 \(u\) 取假的情况。
举个例子,如果题目中说 \(u\) 真或 \(v\) 真。那么其实等价于,若 \(u\) 假,则 \(v\) 真,若 \(v\) 假,则 \(u\) 真,那么从 \(u+n\) 连向 \(v\),从 \(v+n\) 连向 \(u\) 即可。
再讨论一种。比如如果条件为 \(u\) 真或 \(v\) 假。那么就等价于,若 \(u\) 假则 \(v\) 假,若 \(v\) 真则 \(u\) 真,那么从 \(u+n\) 连向 \(v+n\),从 \(v\) 连向 \(u\) 即可。
对于不合法的情况,很简单,就是若 \(u\) 和 \(u+n\) 在同一个强联通分量里面,也就是出现了 若 \(u\) 真,则 \(u\) 假这样的矛盾行为,则就是无解。
对于构造解这种行为。那么其实我们有一种非常显然的贪心,就是我们要让这个值影响的越少越好。于是我们考虑刻画“越少越好”这一想法。其实就是在缩点后的拓扑序上,编号越大的越好。但是由于你用了 Tarjan 缩点,所以你先缩的强联通分量必然是靠近叶子的。于是你得到的恰好就接近于拓扑逆序。
于是我们只关注强连通分量的编号就好了。具体的,如果 \(u\) 的编号比 \(u+n\) 大,那么 \(u\) 取 \(1\),否则取 \(0\)。做完了。
017. 【模板】2-SAT
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2000005;
vector<int> Adj[MAXN];
int n, m, dfn[MAXN], SCCNO[MAXN], SCCcnt = 0, sizSCC[MAXN], low[MAXN], Stk[MAXN], Top = 0, dfsTime = 0;
void Tarjan(int u)
{
Stk[++ Top] = u; dfn[u] = low[u] = ++ dfsTime;
for(int i = 0; i < Adj[u].size(); i ++)
{
int v = Adj[u][i];
if(!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(!SCCNO[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
sizSCC[++ SCCcnt] = 1; SCCNO[u] = SCCcnt;
while(Stk[Top] != u)
{
SCCNO[Stk[Top]] = SCCcnt;
sizSCC[SCCcnt] ++;
Top --;
}
Top --;
}
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
int u, a, v, b;
cin >> u >> a >> v >> b;
Adj[u + (a ^ 1) * n].push_back(v + b * n);
Adj[v + (b ^ 1) * n].push_back(u + a * n);
}
for(int i = 1; i <= (n << 1); i ++)
{
if(dfn[i]) continue;
dfsTime = 0; Tarjan(i);
}
for(int i = 1; i <= n; i ++)
{
if(SCCNO[i] == SCCNO[i + n])
{
cout << "IMPOSSIBLE" << '\n';
return 0;
}
}
cout << "POSSIBLE" << '\n';
for(int i = 1; i <= n; i ++) cout << (SCCNO[i] > SCCNO[i + n]) << " ";
cout << '\n';
return 0;
}
莫队
莫队是一种优雅的暴力。真就是暴力。
考虑离线所有询问,进行一定排序后暴力从一个询问按格转移到下一个区间。如从 \([2,3]\) 转移到 \([3,4]\),那么就加上 \(4\) 的贡献,变为 \([2,4]\),然后删去 \(2\) 的贡献,变为 \([3,4]\),就是这样的。
当然纯暴力是不行的。我们需要找一种让扩展次数尽量少的方法。其实是曼哈顿距离最小生成树。但由于求最小生成树的复杂度过高。莫队给了我们一种非常神奇的区间排序方法。
考虑将序列分块,每次比较两个区间的时候,优先比较他们左端点的所在块,如果块不同,那么块小的排在前面,如果相等,那么右端点大的排在前面。
证明你可以把一个区间 \([L,R]\) 映射为二维平面坐标系的一个点 \((l,r)\),这是类扫描线算法的一个基础思想。然后你尝试跑一遍就可以很容易的看出来复杂度是 \(O(n\sqrt{n})\)(假设 \(n\),\(m\) 同阶)。
使用莫队算法的前提是你可以快速地扩展一个端点,可以快速的删除一个点。如果不能快速做到其中之一,那么可能需要一种莫队的变种——回滚莫队来解决这类问题。
019. P2709 小B的询问
难度:提高+ / 省选-
算是莫队的模板题了。
考虑扩展一个点:就是开一个桶统计数的出现次数。答案计算可以拆完全平方式:\((c_i+1)^2-c_i^2=2\times c_i+1\)。
考虑删除一个点:也是统计出现次数。答案计算也可以拆完全平方式:\(c_i^{2}-(c_i-1)^2=2\times c_i-1\)
代码实现:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 50005;
int n, m, k, A[MAXN], LenBlock, Blo[MAXN], cnt[MAXN], ans = 0, Ans[MAXN];
struct Qry
{
int L, R, id;
} Q[MAXN];
bool Cmp(Qry u, Qry v)
{
if(Blo[u.L] == Blo[v.L]) return u.R < v.R;
return Blo[u.L] < Blo[v.L];
}
void Add(int x)
{
cnt[A[x]] ++;
ans += 2 * cnt[A[x]] - 1;
return;
}
void Del(int x)
{
cnt[A[x]] --;
ans -= 2 * cnt[A[x]] + 1;
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k; LenBlock = sqrt(n);
for(int i = 1; i <= n; i ++) Blo[i] = (i - 1) / LenBlock + 1;
for(int i = 1; i <= n; i ++) cin >> A[i];
for(int i = 1; i <= m; i ++) { cin >> Q[i].L >> Q[i].R; Q[i].id = i; }
sort(Q + 1, Q + m + 1, Cmp);
int nowL = 1, nowR = 0;
for(int i = 1; i <= m; i ++)
{
while(nowL > Q[i].L) Add(-- nowL);
while(nowR < Q[i].R) Add(++ nowR);
while(nowL < Q[i].L) Del(nowL ++);
while(nowR > Q[i].R) Del(nowR --);
Ans[Q[i].id] = ans;
}
for(int i = 1; i <= m; i ++) cout << Ans[i] << '\n';
return 0;
}
019. P1494 [国家集训队] 小 Z 的袜子
难度:提高+ / 省选-
也是板子。先考虑概率转计数。每次选的总数是固定的,就是 \(\binom{cnt}{2}\)。
维护方面也是维护一个桶统计出现次数。每种颜色的贡献是 \(\binom{scnt}{2}\)。随便维护一下这个即可。
020. P4462 [CQOI2018] 异或序列
难度:提高+ / 省选-
板子,继续开桶维护。考虑到异或可以前缀和,于是维护区间异或等价于维护前缀异或。然后做完了。
*021. P4137 Rmq Problem / mex
难度:省选 / NOI-
莫队+值域分块科技题。考虑修改,非常简单就是开桶维护。
考察 mex 的性质。就是从 \(0\) 开始找,找到的第一个 \(i\) 满足 \(cnt_i=0\) 的就是区间 mex。暴力找的询问肯定是复杂度假的。于是我们考虑对值域分块。
考虑记另一个桶,记每一块有多少个数出现。询问时,如果这个块有的数个数等于块长,那么不可能存在 mex,直接跳过。否则块内暴力查找。
这样最多查 \(\sqrt{n}\) 个块。最多块内查 \(\sqrt{n}\) 次。复杂度为 \(O(n\sqrt{n}+n\sqrt{V})\),可以通过。这个复杂度分析可以沿用到重链上二分上面。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL MAXN = 200005;
LL n, m, A[MAXN], LenBlock, B = 500, Blo[MAXN], cntBlo[MAXN], cnt[MAXN], ans[MAXN];
struct Qry
{
LL L, R, id;
} Q[MAXN];
bool Cmp(Qry x, Qry y)
{
if(Blo[x.L] != Blo[y.L]) return Blo[x.L] < Blo[y.L];
else return x.R < y.R;
}
void Add(LL x)
{
cnt[A[x]] ++;
if(cnt[A[x]] == 1) cntBlo[A[x] / B] ++;
return;
}
void Del(LL x)
{
cnt[A[x]] --;
if(cnt[A[x]] == 0) cntBlo[A[x] / B] --;
return;
}
LL Query() // 值域分块
{
LL res = MAXN;
for(LL i = 1; i <= B; i ++)
{
if(cntBlo[i - 1] == B) continue;
for(LL j = (i - 1) * B; j < i * B; j ++)
if(!cnt[j]) { res = j; return res; }
}
return res;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m; LenBlock = sqrt(n);
for(LL i = 1; i <= n; i ++) Blo[i] = (i - 1) / LenBlock + 1;
for(LL i = 1; i <= n; i ++) { cin >> A[i]; }
for(LL i = 1; i <= m; i ++) { cin >> Q[i].L >> Q[i].R; Q[i].id = i; }
sort(Q + 1, Q + m + 1, Cmp);
LL nowL = 1, nowR = 0;
for(LL i = 1; i <= m; i ++)
{
while(nowL > Q[i].L) Add(-- nowL);
while(nowR < Q[i].R) Add(++ nowR);
while(nowL < Q[i].L) Del(nowL ++);
while(nowR > Q[i].R) Del(nowR --);
ans[Q[i].id] = Query();
}
for(LL i = 1; i <= m; i ++) cout << ans[i] << '\n';
return 0;
}
022. P4396 [AHOI2013] 作业
难度:省选 / NOI-
非常简单的题。也是莫队+加值域分块科技。唯一需要注意的就是询问区间长度小于 \(\sqrt{n}\) 的时候需要暴力查找,因为它可能不会触发任何一条查找操作。
023. P3730 曼哈顿交易
难度:省选 / NOI-
另类值域分块。这个值域分块考虑维护热度。扩展和删除的时候稍显麻烦,不过也很套路就是了。
void Add(int x)
{
nowcnt ++;
-- cnt2[cnt[A[x]]];
-- cntBlo[cnt[A[x]] / B];
++ cnt2[++ cnt[A[x]]];
++ cntBlo[cnt[A[x]] / B];
return;
}
void Del(int x)
{
-- cnt2[cnt[A[x]]];
-- cntBlo[cnt[A[x]] / B];
++ cnt2[-- cnt[A[x]]];
++ cntBlo[cnt[A[x]] / B];
return;
}
支持修改的莫队(带修莫队)
就是高维莫队。考虑在每个询问上记录这个询问前面已经有多少次修改了。
然后在扩展左右端点的基础上,再加一个扩展修改位置即可。注意修改的时候判断修改点在询问区间里面才能修改。
024. P1903 [国家集训队] 数颜色 / 维护队列
难度:提高+ / 省选-
板子题:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2000005;
int n, m, A[MAXN], LenBlock, Blo[MAXN], cnt[MAXN], cntQ = 0, cntC = 0, anss = 0, ans[MAXN];
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); }
return x * f;
}
struct Qry
{
int L, R, tim, id;
} Q[MAXN];
struct Updat
{
int Pos, col;
} C[MAXN];
bool Cmp(Qry x, Qry y)
{
if(Blo[x.L] != Blo[y.L]) return Blo[x.L] < Blo[y.L];
if(Blo[x.R] != Blo[y.R]) return Blo[x.R] < Blo[y.R];
return x.tim < y.tim;
}
void Add(int x)
{
cnt[A[x]] ++;
if(cnt[A[x]] == 1) anss ++;
return;
}
void Del(int x)
{
cnt[A[x]] --;
if(cnt[A[x]] == 0) anss --;
return;
}
void Solve(int x, int i)
{
if(C[x].Pos >= Q[i].L && C[x].Pos <= Q[i].R)
{
cnt[A[C[x].Pos]] --; if(cnt[A[C[x].Pos]] == 0) anss --;
cnt[C[x].col] ++; if(cnt[C[x].col] == 1) anss ++;
}
swap(C[x].col, A[C[x].Pos]);
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> A[i];
for(int i = 1; i <= m; i ++)
{
char Opt; cin >> Opt;
if(Opt == 'Q')
{
cntQ ++;
cin >> Q[cntQ].L >> Q[cntQ].R; Q[cntQ].id = cntQ; Q[cntQ].tim = cntC;
}
else
{
cntC ++;
cin >> C[cntC].Pos >> C[cntC].col;
}
}
LenBlock = pow(n, 0.66);
for(int i = 1; i <= n; i ++) Blo[i] = (i - 1) / LenBlock + 1;
sort(Q + 1, Q + cntQ + 1, Cmp);
int nowL = 1, nowR = 0, tt = 0;
for(int i = 1; i <= cntQ; i ++)
{
while(nowL > Q[i].L) Add(-- nowL);
while(nowR < Q[i].R) Add(++ nowR);
while(nowL < Q[i].L) Del(nowL ++);
while(nowR > Q[i].R) Del(nowR --);
while(tt < Q[i].tim) Solve(++ tt, i);
while(tt > Q[i].tim) Solve(tt --, i);
ans[Q[i].id] = anss;
}
for(int i = 1; i <= cntQ; i ++) cout << ans[i] << '\n';
return 0;
}
025. P2464 [SDOI2008] 郁闷的小 J
难度:提高+ / 省选-
一样的板子题,注意先离散化一下就好。
026. P4151 [WC2011] 最大XOR和路径
难度:省选 / NOI-
一个非常重要有启发性的结论是:图上路径可以拆成一棵 DFS 树套上很多环的形式。
然后我们从 \(1\) 开始,任意遍历出一棵 DFS 树,然后暴力把遇到的环插入异或线性基里面。
最后我们的答案就是 DFS 树的答案异或上所有环的异或最大值。所以线性基求一下异或最大即可。然后做完了。

浙公网安备 33010602011771号