就整合成一篇文章吧,从2022/10/31日开始的cf题都放在这里了。
CF428D Random Function and Tree
*2700
树形DP,容斥(正难则反)
这编号代表的是dfs序的奇偶性,所以状态:\(f[u][0/1]\):表示子树内sz的奇偶性,这个转化也很常规。
假如说只有顺序,转移:\(f[u][0]=f[u][1]*f[v][0]+f[u][0]*f[v][1]\),\(f[u][1]\)同理。
所以正序答案*2,再去重。
发现从左往右和从右往左节点编号相同的方案当且仅当:所有子树大小为偶,或者所有子树大小为奇且子树个数为奇。
- 证明:首先如果子树大小奇偶并存,那么一定存在\(i\)和\(i+1\)奇偶性不同,所以\(i\)或\(i+1\)一定有一个的前后缀奇偶性不同,那显然该子树进出时的奇偶性不同。如果是每个子树是奇数,每个儿子进去时的编号一定是奇偶交替,形如10101,要回文显然子树个数为奇数个。code
CF983E NN COUNTRY(主席树,二维数点)
[CF1012E] CYCLE SORT / [EJOI2018] 循环排序(构造,欧拉回路)
CF1720D2 XOR-SUBSEQUENCE (HARD VERSION) [TRIE树+DP]
CF1389G DIRECTING EDGES(边双联通,换根DP)
CF629E FAMIL DOOR AND ROADS(小分类,换根DP)
CF377D DEVELOPING GAME(线段树,扫描线)
CF331C3 THE GREAT JULYA CALENDAR
CF1681F UNIQUE OCCURRENCES
CODEFORCES ROUND #793 (DIV. 2)
CF487C
构造题
两种问题:构造长度为\(n\)的排列,满足:
1.前缀和构成了 \(mod\ n\) 的完全剩余系。
\(n\)为偶数,且偶数下标为\(i-1\),奇数下标为\(n-i+1\)。
2.前缀积构成了 \(mod\ n\) 的完全剩余系。
从整体到一般:最后一位前缀积确定为\(n!\),所以\(n\)只能放在最后一位。同理:\(1\)只能放在开头。
如果\(n\)为\(\ge 4\)的合数,则不可行。
中间部分第\(i\)位为\(\frac{i}{i-1}\)
可以证明:互不相同,因为把每个数减\(1\)得到真分数:\(1,\frac{1}{2},\frac{1}{3},\frac{1}{4}...\),逆元唯一。
CF321E Ciel and Gondolas
wqs二分,决策单调性,单调队列
可以递推预处理出贡献值\(w(i,j)\)。然后发现这道题有决策单调性(四边形不等式法则可以用二维平面上的矩形来理解),可以整体二分\(k\)层,\(O(nklogn)\)
还能做到\(O(n^2)\),不过不会
还有一种很常规的\(O(nlognlogmxval)\)的思路。
发现又是跟个数有关的凸函数。
可以wqs二分,然后单调队列。
我没有考虑第二关键字,感觉比较麻烦,不过不要问我为什么就切了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 4005;
typedef long long ll;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline int rd() {
int x = 0, f = 1; char ch = gc();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = gc();}
while(isdigit(ch)) {x = x * 10 + (ch ^ 48); ch = gc();}
return x * f;
}
int U[N][N], w[N][N], Q[N], lim[N], n, k, mid, pos[N], cnt;
ll dp[N];
ll calc(int i, int j) {return dp[j] + w[j + 1][i] - mid;}
int gt_Mid(int i, int j) {
int l = j + 1, r = n, bst = j;
while(l <= r) {
int md = (l + r) >> 1;
if(calc(md, i) <= calc(md, j)) {l = md + 1; bst = md;}
else {r = md - 1;}
}
return bst;
}
void solve() {
// puts("~~~~~~~~~~~~~~");
int hd, tl;
dp[0] = Q[hd = tl = 1] = 0;
for(int i = 1; i <= n; i++) {
while(hd < tl && lim[hd] < i) hd++;
dp[i] = calc(i, Q[hd]); pos[i] = Q[hd];
// printf("dp[%d]=%d pos=%d\n", i, dp[i], pos[i]);
while(hd < tl && gt_Mid(Q[tl], i) <= lim[tl - 1]) {tl--;} //可以完全替代 Q[tl],两个决策点贡献值相同时,选靠后的
lim[tl] = gt_Mid(Q[tl], i); Q[++tl] = i;
}
cnt = 0;
for(int i = n; i; i = pos[i]) {cnt++;}
// printf("k=%d cnt=%d b=%d\n", mid, cnt, dp[n]);
}
int main() {
// freopen("data.in", "r", stdin);
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {U[i][j] = U[i][j - 1] + rd();}
}
for(int i = 1; i <= n; i++) {
for(int j = i + 1; j <= n; j++) w[i][j] = w[i][j - 1] + U[j][j - 1] - U[j][i - 1];
}
int l = -w[1][n], r = 0; ll bst;
bool flag = 0;
while(l <= r) {
mid = (l + r) >> 1;
solve();
// if(k == 700) printf("%d:%lld ", cnt, dp[n] + 1ll * mid * k);
if(cnt <= k) {bst = dp[n] + 1ll * mid * k; if(cnt == k) {flag = 1; break;} l = mid + 1;}
else {r = mid - 1;}
}
// if(bst == 43636) {printf("!fl=%d\n", flag);}
printf("%lld", bst);
return 0;
}
CF986E
突破口:gcd相当于分解质因数后的质因子取min。
解决这个问题还要想到:树上差分(拆乘到跟的路径的问题)+离线+类似扫描线(就是在树上遍历经过一个点吧它动态加入,离开一个点时回溯删除)。
很多时候,直接考虑暴力枚举什么?经过分析会发现复杂度会比想象的优。
这里对于询问:枚举质因子,以及它的次幂,总复杂度 \(qlogval\)。
这里min不枚举怎么做呢?
CF1383E
*2800
考虑01/00/10都是删除一个0,而只有11删掉的是1,所以可以重点考虑0。
把一个01串用1隔成若干段,每段权值表示\(0\)的个数,双射成一个序列(每个值代表两个\(1\)间隔的\(0\)段,可为空)。
操作相当于将某个非负值-1,或者删除(除了两端,因为最后至少会剩下一个\(1\))一个零。
可以推出:\(s\)序列能得到\(t\)序列的充要条件是:存在\(s\)长为\(|t|\)的子序列,从前往后对应的值都大于等于\(t\)的值。
又可以把\(t\)双射成匹配操作:(贪心)从前往后每次匹配能匹配种的最靠前的一个。
计数操作可以用dp解决了:\(f(i)=\sum\limits_{0}^{j-1}f(j)*max(0,s_i-max_{k=j+1}^{i-1}s_k)\)
这种带最大值的dp直接用单调栈就好了,维护后缀最大值,发现刚好栈中弹掉的部分就是决策。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
const int mod = 1e9 + 7;
char s[N];
int n, b[N], m, st[N], tp;
ll f[N];
int main() {
scanf("%s", s + 1); n = strlen(s + 1);
int tot = 0;
for(int i = 1; i <= n; i++) {
if(s[i] == '1') {b[++m] = tot; tot = 0;}
else tot++;
}
if(!m) {printf("%d\n", n); return 0;}
ll ans = 1ll * (b[1] + 1) * (tot + 1) % mod;
f[1] = 1;
for(int i = 2; i <= m; i++) {
ll res = (f[i - 1] - f[i - 2]) * (b[i] + 1) % mod;
while(tp && b[st[tp]] <= b[i]) {
res = (res + (f[st[tp] - 1] - f[max(0, st[tp - 1] - 1)]) * (b[i] - b[st[tp]])) % mod;
tp--;
}
f[i] = (f[i - 1] + res) % mod;
st[++tp] = i;
}
printf("%lld", (ans * f[m] % mod + mod) % mod);<details>
<summary>点击查看代码</summary>
</details>
return 0;
}
CF582D Number of Binominal Coefficients
*3300
Kummer定理: \(\binom{n + m}{n}\)含\(p\)因子个数为\(n+m\)\(p\)进制下进位的次数
所以问题转化为:\(n+m\)在\(p\)进制下进位次数\(\ge a\)的\((n,m)\)数对数量。
数位DP,具体看这篇
CF772D
*2700
- 题意:满足每位最小值恰为\(x\)的所有集合的带权和(集合内所有数求和的平方和)。
这题\(k\)进制fmt(高位前缀和)+ 合并平方式
都是遇到过的套路,但还是没想出来,也不知道在哪里卡壳了。
这东西如果是二进制就是按位与,所以很容易想到(容斥)至少为\(x\)(大于等于\(x\)的每位)的所有数集的平方和,这个求得了可以通过高维差分反演回来。
初始每个数都可给本身值“恰好”为该值的平方。考虑高维前缀和合并。合并\(s1\)和\(s2\),相当于分别从\(s1\)和\(s2\)任选两个集合贡献\((x1+y1)^2\),拆开发现只需要维护平方和,和,值的个数。合并看每一项贡献的次数,也好想。 - code
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const int N = 1e6 + 5;
ll pw2[N], g[N];
struct rose {
ll f1, f2, f3;
}f[N];
inline rose operator* (rose u, rose v) {
return (rose) {(u.f1 * pw2[v.f3] % mod + v.f1 * pw2[u.f3] % mod + 2 * u.f2 * v.f2 % mod) % mod, (u.f2 * pw2[v.f3] + v.f2 * pw2[u.f3]) % mod, u.f3 + v.f3};
}
int main() {
int n;
scanf("%d", &n);
pw2[0] = 1;
for(int i = 1; i <= n; i++) {
pw2[i] = pw2[i - 1] * 2 % mod;
int x; scanf("%d", &x);
f[x] = f[x] * (rose){1ll * x * x % mod, x, 1};
}
int l = 1;
for(int i = 0; i < 6; i++) {
for(int j = 999999; j >= 0; j--) {
if(j / l % 10 < 9) {
f[j] = f[j] * f[j + l];
}
}
l *= 10;
}
for(int i = 0; i <= 999999; i++) {g[i] = f[i].f1 % mod;}
l = 1;
for(int i = 0; i < 6; i++) {
for(int j = 0; j <= 999999; j++) {
if(j / l % 10 < 9) {
g[j] = (g[j] - g[j + l] + mod) % mod;
}
}
l *= 10;
}
ll ans = 0;
for(int i = 0; i <= 999999; i++) {
ans ^= (g[i] + mod) % mod * i;
}
printf("%lld\n", ans);
return 0;
}
CF1361C Johnny and Megan's Necklace
首先答案只有\(21\)种,可以直接枚举,当然也可以二分。
可以再连的边的前提是\(2^k|(u\ xor\ v)\),其实就是\(u\)和\(v\)的后\(k\)位相同。
如果把连边等价于合并,那么我们原来的每个点\(x\)用\(x\ mod\ 2^k\)代替。
此时之后连的边就相当于把多个不同但\(mod\ 2^k\)相同的点和成一个点了,同时连最初的\(n\)条边,发现如果能找到一条欧拉回路,那就找到方案了。这样做保证了两条边相连的点一定是本来不同但合并在一起的点。
- code
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
int n, X[N], Y[N], head[N], nxt[N], ecnt = 1, to[N], st[N], ed[N], ans[N], tot, du[N], res[N];
namespace IO {
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline int rd() {
int x = 0; char ch = gc();
while(!isdigit(ch)) {ch = gc();}
while(isdigit(ch)) {x = x * 10 + (ch ^ 48); ch = gc();}
return x;
}
}
void add_edge(int u, int v, int x, int y) {du[u]++; nxt[++ecnt] = head[u]; to[ecnt] = v; st[ecnt] = x; ed[ecnt] = y; head[u] = ecnt;}
bool vis[N];
void solve(int u) {
for(int i = head[u]; i; i = nxt[i]) {
head[u] = i;
if(vis[i]) continue; vis[i] = vis[i ^ 1] = 1;
int v = to[i];
solve(v);
ans[++tot] = ed[i]; ans[++tot] = st[i];
i = head[u];
}
}
bool check(int mid) {
tot = 0; ecnt = 1; for(int i = 0, up = 1 << 20; i <= up; i++) {du[i] = head[i] = vis[i] = 0;}
int mod = 1 << mid;
for(int i = 1; i <= n; i++) {int x(X[i] % mod), y(Y[i] % mod); add_edge(y, x, i * 2, i * 2 - 1); add_edge(x, y, i * 2 - 1, i * 2);}
for(int i = 0; i < mod; i++) {if(du[i] & 1) {return 0;}}
solve(X[1] % mod);
return (tot == (n << 1));
}
int main() {
n = IO::rd();
for(int i = 1; i <= n; i++) {X[i] = IO::rd(); Y[i] = IO::rd();}
int l = 0, r = 20, bst = -1;
while(l <= r) {
int mid = (l + r) >> 1;
if(check(mid)) {
bst = mid; l = mid + 1;
for(int i = 1; i <= (n << 1); i++) {res[i] = ans[i];}
}
else r = mid - 1;
}
printf("%d\n", bst);
for(int i = 1; i <= (n << 1); i++) {printf("%d ", res[i]);}
return 0;
}
CF1149C Tree Generator
*2700
动态维护可交换下括号序列对应的括号树直径。
直径是一条路径,可以表示为\(dep(u)+dep(v)-2*dep(lca(u,v))\)。而\(dep(lca(u,v))\) 在括号序列上为\(\min\limits_{i=u}^v dep(i)\)。所以可以用线段树维护\(dep\)的区间区间最大值,\(dep(i)-2*\min\limits_{j=l}^i dep(j)\)的最大值,以及\(dep(i)-2*\min\limits_{j=i}^r dep(j)\)的最大值。相当于把柿子拆成两份,合并\(ans\)直接看\(lca\)在左右哪半边了。
对于修改,设左括号为\(-1\),右括号为\(1\),由于\(dep\)实际在括号序上相当于求前缀和,因此交换两个括号会使得区间前缀(深度)\(+/- 2\)。
CF856C Eleventh Birthday
一个数\(mod\ 11\)同余它的奇数位和减去偶数位和。
可以预处理出每个数的奇减偶和,如果后面有奇数位,那相当于取相反数。
发现位数为偶的数不影响其余位的奇偶性。
根据位数奇偶性的不同,分别按下面dp。
设\(dp(i,j,k)\)表示前\(i\)个数,有\(j\)个是正,奇减偶\(mod\ 11=k\)的方案数。
这个\(dp\)得到的值为每个数为正/负的组合数。
然后正负相同的分别乘上排列数。
再将位数为偶的用插板法计入(也按正负分开计算后乘起来)。
CF1420E Battle Lemmings
*2700
思路本来不太难想的,我以来就开始猜贪心结论了,其实dp的优先级别是高于贪心的,无论是套路性还有正确性上。
后缀\(i\),决策为删或者不删,在\(s_i!=s_{i+1}\)时,显然不删,那就是\(i+1\)的后缀加上\(s_i\)更新过来。
否则判断后缀\(i+2\)的答案从前往后第一位不等于\(s_i\)的位与\(s_i\)的大小关心(小于则可以删)。
这些信息都是可以从后往前递推的,答案不超过\(11\)位也可以用string保存。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
char s[N];
string L[N], R[N];
int n, g[N], len[N], a[N]; //g[i]: 之后第一个和 后缀 i答案第一位不同的大小关系
void Chg(int i) { //len=11
R[i] = L[i].substr(9, 2);
L[i] = L[i].substr(0, 5);
}
int Hash(int l) {return a[l] * 27 + a[l + 1];}
bool check(int i, int j) {
if(s[i] == L[j][0]) {return g[j];}
else return s[i] > L[j][0];
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
for(int i = 1; i <= n; i++) a[i] = s[i] - 'a' + 1;
len[n] = 1; L[n] += s[n]; g[n] = 1;
for(int i = n - 1; i; i--) {
if(s[i] == s[i + 1] && check(i, i + 2)) {
g[i] = g[i + 2];
len[i] = len[i + 2];
L[i] = L[i + 2]; R[i] = R[i + 2];
}
else {
len[i] = len[i + 1] + 1;
g[i] = check(i, i + 1);
if(len[i + 1] > 10) {
R[i] = R[i + 1];
L[i] = s[i] + L[i + 1].substr(0, 4);
}
else {
L[i] = s[i] + L[i + 1];
if(len[i] == 11) {Chg(i);}
}
}
}
for(int i = 1; i <= n; i++) {
printf("%d ", len[i]);
if(len[i] <= 10) {printf("%s\n", L[i].c_str());}
else {printf("%s...%s\n", L[i].c_str(), R[i].c_str());}
}
return 0;
}
CF1268E Happy Cactus
题意可以理解成:每条边向比它边权大的边连边,求每个条边能到的边个数。
如果仙人掌退化为树的话,直接拓扑序列排序,从大到小加边,每次加边\((u,v)\)时,共通\(u\)和\(v\)分别能到的点,\(f(u)=f(v)=f'(u)+f'(v)+1\)。
但实际仙人掌可能会算重,具体某条边在加\((u,v)\)前,\(u\)和\(v\)都能够到达,在该环内找这种边发现只可能是环内边权最大边。所以存在重复即存在\(u\)和\(v\)都能走连续递增的边到达最大边,此时重复的点数为最大边的两端及两端能到的环外点=加最大边前的\(f(u)+f(v)\),减掉就好。
CF1528E Mashtali and Hagh Trees
*3100
首先第三个条件的转化我并没有看出来,我应该多手玩一下。
计数内向树和外向树拼起来。具体思路就是递推然后根据度数,子树是否达到最高深度(因为同时多个子树取同一类方案可能会算重)推一推就好了,我反正目前没有推准的能力。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
const int mod = 998244353;
ll ksm(ll a, ll b) {ll res(1); for(; b; b >>= 1, a = a * a % mod)if(b & 1) res = res * a % mod; return res;}
ll f[N], s[N], inv6 = ksm(6, mod - 2);
ll C2(int n) {return 1ll * n * (n - 1) / 2 % mod;}
ll C3(int n) {return 1ll * n * (n - 1) % mod * (n - 2) % mod * inv6 % mod;}
ll f_(int x) {return !x ? 0 : (f[x] - f[x - 1]);}
int main() {
int n; scanf("%d", &n);
s[0] = f[0] = 1;
for(int i = 1; i <= n; i++) f[i] = (f[i - 1] + f[i - 1] * (i < 2 ? 0 : s[i - 2]) + C2(f[i - 1] + 1)) % mod, s[i] = (s[i - 1] + f[i]) % mod;
ll ans = (f[n] + f[n - 1] * C2(s[n - 2] + 1) + s[n - 2] * C2(f[n - 1] + 1) + C3(f[n - 1] + 2)) % mod;
ans = (ans * 2 - 1) % mod;
for(int i = 0; i < n; i++) ans = (ans + (f[i] - 1) * f_(n - 1 - i)) % mod;
printf("%lld", (ans + mod) % mod);
return 0;
}
CF1556G Gates to Another World
所有连边等价于线段树上所有非叶子的左右子树对应位相连。
这里又有是一些区间操作,也印证了线段树的做法。
这里有两个问题:1.维护联通性?时光倒流+加边并查集合并
2.边数点数太多?动态开点线段树,保证每个叶子节点管辖的区间点的存在时间相同,这些点同时存在时也都是联通的。
加边复杂度是每个非叶子节点左右子树和,和线段树合并一模一样为:\(n^2m\)。
所以流程为:先区间覆盖打标记,得到每个叶子节点存在时间,再连边(边的存在时间为两端节点先删除的那个之前)。
再倒流扫一遍即使并查集连边合并就好了。
CF1523H Hopping Around the Array
倍增+背包
正常倍增预处理能跳到的最靠右的地方。
求答案时,设 \(f(i)\) 表示跳 \(i\) 次,跳到的最远点,显然单增。答案 \(ans\) 为第一个 \(f\ge R\) 的次数,不好从小到大或者二分枚举求,可以改为求\(ans-1\)满足 \(f<R\)的最大次数,因此可以贪心从高到低位取。
不过TLE,QaQ
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 5;
const int M = 35;
struct query {int l, r, k;} Q[N];
int n, q, a[N], mx[N][M][17], f[N][M][17], lg[N], K, ans[N], g[N][M], pos[M];
int MX(int l, int r, int j) {
int t = lg[r - l + 1];
return max(mx[l][j][t], mx[r - (1 << t) + 1][j][t]);
}
void _ST() {
for(int t = 1; t <= lg[n]; t++) {
int l = 1 << (t - 1);
for(int i = 1, up = n - l * 2 + 1; i <= up; i++)
for(int j = 0; j <= K; j++) {mx[i][j][t] = max(mx[i][j][t - 1], mx[i + l][j][t - 1]);}
}
}
void init() {
lg[1] = 0; for(int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
for(int i = 1; i <= n; i++) for(int j = 0; j <= n; j++) f[i][j][0] = min(n, i + a[i] + j);
for(int k = 1; k <= lg[n]; k++) {
for(int i = 1; i <= n; i++) for(int j = 0; j <= K; j++) {mx[i][j][0] = f[i][j][k - 1];}
_ST();
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= K; j++) {
for(int t = 0; t <= j; t++) f[i][j][k] = max(f[i][j][k], MX(i, f[i][t][k - 1], j - t));
}
}
}
}
int main() {
scanf("%d%d", &n, &q); K = min(30, n);
for(int i = 1; i <= n; i++) {scanf("%d", &a[i]);}
init();
for(int i = 1; i <= q; i++) {
scanf("%d%d%d", &Q[i].l, &Q[i].r, &Q[i].k);
if(Q[i].l == Q[i].r) {ans[i] = -1;}
for(int j = 0; j <= Q[i].k; j++) g[i][j] = Q[i].l;
}
for(int k = lg[n]; k >= 0; k--) {
for(int i = 1; i <= n; i++) for(int j = 0; j <= K; j++) {mx[i][j][0] = f[i][j][k];}
_ST();
for(int i = 1; i <= q; i++) {
if(ans[i] == -1 || f[g[i][0]][Q[i].k][k] >= Q[i].r || lg[Q[i].r - Q[i].l] < k) continue;
for(int j = 0; j <= Q[i].k; j++) {
pos[j] = 0;
for(int t = 0; t <= j; t++) {
if(!t || g[i][t] > g[i][t - 1]) pos[j] = max(pos[j], MX(Q[i].l, g[i][t], j - t));
}
}
if(pos[Q[i].k] >= Q[i].r) continue;
ans[i] += (1 << k);
for(int j = 0; j <= Q[i].k; j++) {g[i][j] = pos[j];}
}
}
for(int i = 1; i <= q; i++) printf("%d\n", ans[i] + 1);
return 0;
}
CF1661F Teleporters
设 \(f(len,x)\) 为长度为 \(len\) 的段上加 \(x\) 个传送机的最小花费。
显然分成的 \(x+1\) 段越平均越好,就可以\(O(1)\)直接算了。
回到题目,求解的是\(n\)段的\(f\)值之和。
发现\(f\)函数是有凸性的,\(\Delta = f(len,x-1)-f(len,x)\) 随 \(x\) 增加单减。
所以相当于:每次可以加一个传送机使某个 \(f\) 值减 \(\Delta\),最少次数使得这个 \(n\) 个 \(f\) 相加 \(\le m\)。
贪心地每次选\(n\)个中\(\Delta\)最大的,发现减的 \(\Delta\) 为所有 \(\Delta\) 从大到小排序,连续一段前缀区间。
二分区间最小值 \(dw\) ,对于每个 \(len\) ,找到最大的 \(x\)为加的传送机个数,满足 \(f(len,x-1)-f(len,x)\ge dw\)。\(g(dw)\)即为当前花费,且 \(\Delta=dw\) 的全部算入。
外层二分找到最大的 \(g(dw)\le m\) ,这样就找到了所选的最小值 \(dw\),\(\Delta>dw\) 的显然全部算然,但 \(g(dw)\) 把 \(\Delta=dw\) 的全部算入了,其实最后答案可以少算 \((m-g(dw))/dw\) 个。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
ll a[N], m;
int n;
ll f(ll len, int x) {
ll r = len % (x + 1), l = len / (x + 1), l_ = l + (r > 0);
return r * l_ * l_ + (x + 1 - r) * l * l;
}
bool flag;
ll Cnt;
ll g(ll dw) {
Cnt = 0; ll res = 0;
for(int i = 1; i <= n; i++) {
int l = 1, r = a[i], x = 0;
while(l <= r) {
ll mid = (l + r) >> 1;
if(f(a[i], mid - 1) - f(a[i], mid) >= dw) {x = mid; l = mid + 1;}
else r = mid - 1;
}
// if(flag) {printf("!i=%d x=%d\n", i, x);}
Cnt += x;
res += f(a[i], x);
}
return res;
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {scanf("%lld", &a[i]);}
for(int i = n; i; i--) a[i] -= a[i - 1];
scanf("%lld", &m);
ll l = 0, r = 1e18, dw;
while(l <= r) {
ll mid = (l + r) / 2;
if(g(mid) <= m) {dw = mid; l = mid + 1;}
else r = mid - 1;
}
ll w = g(dw);
//printf("!dw=%lld Cnt=%lld w=%lld\n", dw, Cnt, w);
printf("%lld", Cnt - (m - w) / dw);
return 0;
}
CF1687D Cute number
首先手玩平方和数列及好数部分是很有规律的。
枚举 \(a[1]\) 所在的块 \(i\),上限为值域 \(V\) ,把\(a[1]\)先钦定在块左端,\(a\)数列其它点最多存在于两个异色块内,也就是说白块必须移到黑块(\(k\) 下界),黑块不能移动到白块(上界)。
枚举每个黑白块,因为每块长度\(\ge i\),至多枚举 \(\frac{V}{i}\)个,所以复杂度 \(O(VlnV)\)。
实现细节:每个块内最靠右/左,可以找右端点左边第一个/左端点右边第一个,预处理 \(pre/nxt\) 链表。
点击查看代码
using namespace std;
typedef long long ll;
const int N = 4e6 + 5;
int cnt[N], a[N], up = 4e6, pre[N], nxt[N];
int main() {
int n; scanf("%d", &n);
for(int i = 1; i <= n; i++) {scanf("%d", &a[i]); if(i > 1)cnt[a[i] - a[1]]++;}
pre[0] = -1; for(int i = 1; i <= up; i++) if(cnt[i]) pre[i] = i; else pre[i] = pre[i - 1];
nxt[up] = up; for(int i = up - 1; i >= 0; i--) if(cnt[i]) nxt[i] = i; else nxt[i] = nxt[i + 1];
for(ll i = 1; i <= a[n]; i++) {
if(i * (i + 1) < a[1]) continue;
int kl = 0, kr = i; if(a[1] > i * i) kl = a[1] - i * i;
int l = 0, r = i;
for(int j = i; ; j++) {
if(pre[r] >= l) {kr = min(kr, r - pre[r]);}
l += j * 2 + 1;
if(nxt[r + 1] < l) {kl = max(kl, l - nxt[r + 1]);}
r += j * 2 + 2;
if(l > a[n] - a[1]) break;
}
if(kl <= kr) {printf("%lld\n", i * i + kl - a[1]); break;}
}
return 0;
}
CF643G Choosing Ads
问题可以转化为:出现次数前 \(k=\frac{100}{p}\) 大的值。
每次删\(k+1\)个不同的值,最后剩下的一定包含 \(\ge p /%\) 的值,因为出现次数一定严格大于删的次数。
区间查询,直接线段树维护区间前 \(k\le 5\) 大的值,\(k^2\)暴力插入合并。
CF1635F Closest Pair
点对最值。
确定\(w\)更大的点,可以删除一些无用的点对,发现简化后的点对只有\(O(n)\)个,二维数点解决。
CF1083C Max Mex
查询的是最长前缀值,满足能合并成一条路径。
线段树维护值区间 \([l,r]\),合并成的路径,具体合并两条路径可以把另一条路径的两个点依次暴力合并,合并点与路径判断距离即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int a[N], fa[N], head[N], to[N], nxt[N], ecnt, dep[N], dfn[N], In[N], Time, mnd[N][21], lg[N], pos[N];
void add_edge(int u, int v) {nxt[++ecnt] = head[u]; to[ecnt] = v; head[u] = ecnt;}
int MN(int l, int r) {
if(l > r) swap(l, r);
int k = lg[r - l + 1];
return min(mnd[l][k], mnd[r - (1 << k) + 1][k]);
}
int dist(int x, int y) {
return dep[x] + dep[y] - MN(In[x], In[y]) * 2;
}
void _ST() {
for(int i = 1; i <= Time; i++) mnd[i][0] = dep[dfn[i]];
for(int j = 1; j <= lg[Time]; j++) {
int k = 1 << (j - 1);
for(int i = 1, up = Time - k * 2 + 1; i <= up; i++) {
mnd[i][j] = min(mnd[i][j - 1], mnd[i + k][j - 1]);
}
}
}
void init(int u) {
dfn[++Time] = u; In[u] = Time;
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i];
dep[v] = dep[u] + 1; init(v);
dfn[++Time] = u;
}
}
struct node {
int x, y;
inline friend node operator+(node u, int z) {
int x(u.x), y(u.y);
int d1 = dist(x, y), d2 = dist(y, z), d3 = dist(x, z);
if(d1 == d2 + d3) {return (node){x, y};}
if(d2 == d3 + d1) {return (node){y, z};}
if(d3 == d1 + d2) {return (node){x, z};}
return (node){-1, -1};
}
inline friend node operator*(node u, node v) {
if(!u.x) {return v;}
if(u.x < 0 || v.x < 0) {return (node){-1, -1};}
u = u + v.x;
if(u.x < 0) {return u;}
return u + v.y;
}
};
struct seg {
int l, r; node w;
}T[N << 2];
void P_up(int x) {
T[x].w = T[x << 1].w * T[x << 1 | 1].w;
}
void Build(int x, int l, int r) {
T[x].l = l; T[x].r = r;
if(l == r) {T[x].w = (node) {pos[l], pos[l]}; return;}
int mid = (l + r) >> 1;
Build(x << 1, l, mid), Build(x << 1 | 1, mid + 1, r);
P_up(x);
}
void Update(int x, int p) {
if(T[x].l == T[x].r) {T[x].w = (node) {pos[p], pos[p]}; return;}
int mid = (T[x].l + T[x].r) >> 1;
(p <= mid) ? Update(x << 1, p) : Update(x << 1 | 1, p);
P_up(x);
}
node now;
void Ask(int x) {
if(T[x].l == T[x].r) {
if((now * T[x].w).x > 0) {printf("%d\n", T[x].r);}
else printf("%d\n", T[x].r - 1);
return;
}
node tmp = now * T[x << 1].w;
if(tmp.x > 0) {now = tmp; Ask(x << 1 | 1);}
else Ask(x << 1);
}
int main() {
int n, q;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {scanf("%d", &a[i]); pos[++a[i]] = i;}
for(int i = 2; i <= n; i++) scanf("%d", &fa[i]), add_edge(fa[i], i);
init(1);
for(int i = 2; i <= Time; i++) lg[i] = lg[i >> 1] + 1;
_ST(); Build(1, 1, n);
scanf("%d", &q);
for(int i = 1; i <= q; i++) {
int op, x, y;
scanf("%d", &op);
if(op == 1) {
scanf("%d%d", &x, &y);
swap(a[x], a[y]);
pos[a[x]] = x; pos[a[y]] = y;
Update(1, a[x]), Update(1, a[y]);
}
else {
now = (node){0, 0};
Ask(1);
}
}
return 0;
}
CF348E Pilgrims
使一个关键点 \(u\) 的所有最长路径被阻断的点为: 以\(u\)为根,其余最远点集的lca到根的路径上的点。
所以只需要找每个点对应的那个lca即可。
随便钦定一个根,对于关键点\(u\),若最远点既存在于\(u\)子树内又存在于子树外,那么\(lca=u\),\(u\)不可能被阻断。
- 仅存在于子树内: 若仅存在于\(1\)个儿子子树中,继承儿子子树\(lca\);否则,\(lca=u\)。
- 仅存在于子树外:dfs的同时动态维护\(lca\),最远距离等等。
我写的是另一种,用了小trick的一个性质,因为一些细节调了一个上午。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
#define fi first
#define se second
typedef pair<int, int> PII;
int n, m, head[N], to[N], fa[N], S, T, W, nxt[N], len[N], ecnt, cnt[N], sz[N], mxd[N], dep[N];
bool tem[N];
void add_edge(int u, int v, int w) {nxt[++ecnt] = head[u]; to[ecnt] = v; len[ecnt] = w; head[u] = ecnt;}
void dfs(int u, int D) {
if(tem[u] && D > W) {T = u; W = D;}
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i]; if(v == fa[u]) continue;
dep[v] = dep[u] + len[i]; fa[v] = u; dfs(v, D + len[i]);
}
}
void init(int u) {
cnt[u] = sz[u] = tem[u];
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i]; if(v == fa[u]) continue;
fa[v] = u;
init(v); sz[u] += sz[v];
if(!cnt[v]) continue;
int w = mxd[v] + len[i];
if(w > mxd[u]) {mxd[u] = w; cnt[u] = cnt[v];}
else if(w == mxd[u]) {cnt[u] += cnt[v];}
}
}
PII val[3];
int LCA[N], Rt;
void _fd(int u) {
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i]; if(v == fa[u]) continue;
if(mxd[v] + len[i] == mxd[u] && cnt[v] == cnt[u]) {
_fd(v);
return;
}
}
LCA[Rt] = u;
}
int tag[N];
void Sum(int u) {
for(int i = head[u]; i; i = nxt[i]) {
int v = to[i]; if(v == fa[u]) continue;
Sum(v); tag[u] += tag[v];
}
}
int main() {
// freopen("data.in", "r", stdin);
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) {int x; scanf("%d", &x); tem[x] = 1;}
for(int i = 1; i < n; i++) {int u, v, w; scanf("%d%d%d", &u, &v, &w); add_edge(u, v, w), add_edge(v, u, w);}
dfs(1, 0); S = T; dep[S] = W = fa[S] = 0; T = 0; dfs(S, 0);
int rt = T; while(dep[rt] > W / 2) {rt = fa[rt];}
// printf("!S=%d T=%d rt=%d\n", S, T, rt);
fa[rt] = 0; init(rt);
int du = 0;
for(int i = head[rt]; i; i = nxt[i]) {
int v = to[i];
if(!cnt[v]) continue;
du++;
PII tmp = make_pair(mxd[v] + len[i], v);
if(tmp >= val[0]) {val[2] = val[1]; val[1] = val[0]; val[0] = tmp;}
else if(tmp >= val[1]) {val[2] = val[1]; val[1] = tmp;}
else if(tmp > val[2]) {val[2] = tmp;}
Rt = v; _fd(v);
// printf("%d: mxd=%d cnt=%d LCA=%d\n", v, mxd[v], cnt[v], LCA[v]);
}
// for(int t = 0; t < 3; t++) printf("rk=%d (%d,%d)\n", t, val[t].fi, val[t].se);
for(int i = head[rt]; i; i = nxt[i]) {
int v = to[i];
if(!cnt[v]) continue;
int rk1 = (v == val[0].se) ? 1 : 0, rk2 = (v == val[0].se || v == val[1].se) ? 2 : 1;
if(du == 2 || val[rk1].fi != val[rk2].fi) {
// printf("tag[%d]+=%d\n", LCA[val[rk1].se], sz[v]);
tag[LCA[val[rk1].se]] += sz[v]; tag[rt] -= sz[v];
}
}
if(tem[rt]) {
if(val[0].fi != val[1].fi) {tag[LCA[val[0].se]]++; tag[rt]--;}
}
for(int i = 1; i <= n; i++) if(tem[i]) tag[i]++;
// for(int i = 1; i <= n; i++) printf("tag[%d]=%d\n", i, tag[i]);
Sum(rt);
int ans1 = 0, ans2 = 0;
for(int i = 1; i <= n; i++) if(!tem[i]) {
if(ans1 < tag[i]) {ans1 = tag[i]; ans2 = 1;}
else if(ans1 == tag[i]) ans2++;
}
printf("%d %d\n", ans1, ans2);
return 0;
}
CF1237H Balanced Reversals
相邻两个(如 \(00\), \(01\), \(10\), \(11\) )显然可以看成一个值。
构造操作为:从前往后依次匹配 \(T\) 的每一位,已经匹配的部分 \(S\) 是 \(T\)的倒序,这样再翻转到下一个匹配值前就是正序,再加上匹配值翻回前缀。
关键是可能找不到匹配值,此时需要 \(01\) / \(10\) 时,仅存在倒序。
由于操作过程中不翻转非操作位置,所以一开始要 \(S\) 和 \(T\) 中的 \(01\) 个数相同。
仅操作某个 \(S\) 或某个 \(T\) 的前缀即可使个数相等。具体就不证明了。