就整合成一篇文章吧,从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\) 的前缀即可使个数相等。具体就不证明了。