2023 做题记录

就是 2023 做过的个人觉得还不错的题。

1 CF1601D Difficult Mountain
Problem

n 个人相约去爬山。每座山有一个初始的攀登难度 d。每个人有两个属性 a,s,当满足 sidj 时第 i 个人可以爬上第 j 座山,攀上后第 j 座山的高度会变为 max(dj,ai)。问在最优情况下有多少人能爬上山。

n5×105ai,si,di109

2s500MB

Sol

忘了当时是从哪里看到的了。但是那时候猜了一手按 (max(ai,si),si) 的双关键字排序,当时不会证但是过了,太不牛了。

考虑证明这个结论。

先把人分为两类:aisiai>si。两边的集合如果单独贪心的话是简单的,即按照 siai 分别排序贪心即可,记这两个集合分别为 S,T。然后肯定是优先考虑 S 的。考虑 ST 的影响,发现当且仅当 [ai,si][sj,aj]i,j 只能取一个。这时候肯定是取 i,因为 aiaj

所以要按照 max{ai,si} 排序,但这样会有一个小瑕疵,就是有可能存在 sj<aisi=aj 的情况,但这样要求要选 i,所以还要让 si 成为第二关键字。然后就做完了。

#include<bits/stdc++.h>
#define ll long long 
using namespace std;
const int N = 5e5 + 5;
int n, d;
struct node {
	int s, a, mx;
	friend bool operator < (node x, node y) {
		return x.mx != y.mx ? x.mx < y.mx : x.s < y.s;
	}
} a[N];

int main() {
	cin >> n >> d;
	for (int i = 1; i <= n; i++) {
		cin >> a[i].s >> a[i].a;
		a[i].mx = max(a[i].s, a[i].a);
	}
	sort(a + 1, a + n + 1);
	int ans = 0;
	for (int i = 1; i <= n; i++)
		if (d <= a[i].s) {
			ans++;
			d = max(d, a[i].a);
		}
	cout << ans << endl;
	return 0;
}
2 CF690A3 Collective Mindsets (hard)
Problem

一共有 n 个人,当前人的编号是 r,每个人头顶上有一个数字 ai。每个人能看到其他人头顶的数字,所有人需同时给出自己的答案,使得至少有一个人是对的。多测。

T500001r,ain6

4s250MB

Sol

一些构造题如果 ai[1,n],可以考虑一下性质。发现是 n 个人中只需要让一个人猜对,这个东西可以考虑猜测所有数的和,因为一共只有 n 种余数,并且 1ain,所以这样就至少有一个人是对的了。

Code
#include<bits/stdc++.h>
#define ll long long
using namespace std;

int read() {
	int ret = 0, sgn = 0;
	char ch = getchar();
	while (!isdigit(ch)) sgn |= ch == '-', ch = getchar();
	while (isdigit(ch)) ret = ret * 10 + ch - '0', ch = getchar();
	return sgn ? -ret : ret;
}

int main() {
	int T = read();
	while (T--) {
		int n = read(), r = read(), sum = 0;
		for (int i = 1; i < n; i++) {
			int x = read();
			sum += x;
		}
		cout << (r - sum % n + n) % n + 1 << endl;
		// 这里要 +1, 因为之前是按余数算的,但要求的是 1~n。
	}
	return 0;
}
3 CF727F Polycarp's problems
Problem

给定一个长为 n 的数列 am 次询问,每次给出 a0 的值,求至少删去多少个数使任意位置的前缀和 0

n750m2×105|ai|1090bi1015

2s250MB

Ex:存在 O((n+m)logn) 的做法 。

Sol

虽然这道题的 dp 是比较显然的,贪心做法却比较妙。

先说 dp。如果只有一次询问的话,令 fi 表示使前 i 个数满足条件的最小操作次数,但这样转移还需要查询区间最值,而且比较难以优化到多次查询。正难则反,考虑从后面 dp。但是要这个 dp 不能受到 b 的影响,不妨设 fi,j 表示后 i 个数,保留 j 个的最小的 sk。则有转移:fi,j=min(fi+1,j,max(0,fi+1,j1ai))

然后在 f1,i 上二分即可。时间复杂度:O(n2+mlogn)。没有 dp 的代码。

然后说一下贪心的做法。就是有一个贪心就是用一个正数去消多个负数,然后最后剩下的消不掉的就由 a0 去消,最后剩下的负数个数就是操作次数,这样显然是错的,就是在一个整数后面接一个极小(绝对值大)的负数,然后再接一个极大的正数,最后都是极大(绝对值小)的负数。

考虑怎么解决掉这个问题,发现是因为前面的决策后面就考虑不到了,所以就不能局部贪心了,所以倒过来就行了。然后用堆维护并在查询时二分即可做到 O((n+m)logn)

Code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 755;
int n, m;
ll a[N], tp, sum[N];
priority_queue <int> q;

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = n; i; i--) {
		if (a[i] < 0) q.push(a[i]);
		else {
			while (!q.empty() && a[i] >= 0) {
				a[i] += q.top();
				q.pop();
			}
			if (a[i] < 0) q.push(a[i]);
		}
	}
	while (!q.empty()) {
		sum[++tp] = -q.top();
		q.pop();
	}
	for (int i = 1; i <= tp; i++) sum[i] += sum[i - 1];
	while (m--) {
		ll x;
		scanf("%lld", &x);
		int res;
		if (x >= sum[tp]) res = 0;
		else res = tp - (upper_bound(sum + 1, sum + tp + 1, x) - sum) + 1;
		printf("%d\n", res);
	}
	return 0;
}
4 AGC058C Planar Tree
Problem

给定一个 n 个点的环,每个点有权值 ai,当 |aiaj|=1i,j 之间有边,问是否能选出其中 n1 条 在环上除端点外不相交 的边构成一棵树。T 组数据。

T7.5×1044nn3×105ai{1,2,3,4}

2s1GB

Sol

当时看了题解,现在自己做出来了。

首先如果有连续的一段的 a 值时相等的,肯定可以把它们缩成一个点,因为可以同时连向一个相同的点,这样也不会相交。然后发现 2,3 的决策比 1,4 要多,所以要尽量的让 1,4 作为叶子分别连向 2,3。所以如果有 (3,4)/(1,2) 的无序相邻对的话肯定就要消掉。然后这个序列就会剩下形如 (3,2,4),(2,3,1),(3,2,3),(2,3,2) 的段。像 (3,2,4) 这种可以让 2 作为一个叶子,然后抹掉 2,然后 3,4 就相邻了,显然是抹掉 4(2,3,1) 也是同理。

然后这样就可以通过一些操作抹掉所有的 1,4,然后像 (3,2,3) 这种段,显然将 2 作为叶子,(2,3,2) 这种同理,将 3 作为叶子即可,所以只有 2,3 的话是必然有解的。

考虑什么情况下才能消掉所有的 1,4。直接模拟显然不是明智的选择,发现此时消 1 需要先消一个 3,消 4 需要先消掉 2。所以消掉的是 (1,3),(2,4)。记合并相邻的相同项过后,i 的出现次数为 ci。则当且仅当 c1>c3,c2>c4 满足。不能取等的原因是如果先消完 (1,3) 的话,是不能消 (2,4) 的,因为消 (2,4) 至少需要一个 3,然后如果只有一个取等是不可能存在的。

实现上可以先从左到右扫一遍,然后再从右到左做一遍就行。注意枚举到 2n,因为是一个环。

Code
#include<bits/stdc++.h>
using namespace std;
#define Fin(file) freopen(file,"r",stdin)
#define Fout(file) freopen(file,"w",stdout)
#define L(i,j,k) for(int i=(j);i<=(k);++i)
#define R(i,j,k) for(int i=(j);i>=(k);--i)
int n;
int a[300010],vis[300010],c[5];
void solve(){
	cin>>n;
	L(i,1,n)cin>>a[i],vis[i]=0;
	L(_,0,1){
		int pre=-1;
		L(i,1,2*n)if(!vis[(i-1)%n+1]){
			int v=a[(i-1)%n+1];
			if((pre==v)||(pre==2&&v==1)||(pre==3&&v==4))vis[(i-1)%n+1]=1;
			else pre=a[(i-1)%n+1];
		}
		reverse(a+1,a+n+1);reverse(vis+1,vis+n+1);
	}
	L(i,1,4)c[i]=0;
	L(i,1,n)if(!vis[i])++c[a[i]];
	cout<<((c[4]>=c[2]||c[1]>=c[3])?"No":"Yes")<<"\n";
}
int main(){
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	int T;cin>>T;
	while(T--)solve();
	return 0;
}
5 P8565 Sultan Rage
Problem

有一个数列 {an} 满足对 n>m 均有 an=j=1manj,并且 a1,a2,,am 是输入中给出的正整数。

q 次询问,每一次给出一个正整数 x,问有多少个不可重正整数集 S 满足 sSas=x。答案对质数 998244353 取模,多测。

T=52m1001q,ai1001x1018

1s512MB

Sol

发现数列 a 增长的特别快,项数最多时是 a1=a2=1,m=2,也就是斐波那契数列,这样也只需要不到 100 项就可以超过 1018

发现值域上点的分布越来越稀疏且点极少,可以考虑搜索,函数 dfs(val, cur) 表示凑出 x 还需要 val,现在在考虑 cur

但光是搜索肯定不能过这道题,考虑优化。

先记忆化掉重复的操作,可以用一个 map 来存储,然后可以进行可行性剪枝,如果以后再怎么凑都不行,直接剪枝,可以用前缀和优化一下。

但是这样只有 30 分。

可以改变一下搜索的顺序,因为先考虑小元素的话,会有较多的无用的搜索,且小元素较灵活,更容易凑到 x,故可以从大到小的考虑。这样就可以过了。

注意:判断能否返回时是 mp[cur].conut(val),而不是 mp[cur][val],否则会超时。因为如果这样判断的话,会直接额外开一个节点,对每一层都是如此,但第一次搜索时会额外开 2dep1 个节点,会使后面的搜索查询越来越慢(尽管迟早也要开,等于这样使常数变大了不少)。

时空可过。

Code
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
ll read() {
	ll ret = 0, sgn = 0;
	char ch = getchar();
	while (!isdigit(ch)) sgn |= ch == '-', ch = getchar();
	while (isdigit(ch)) ret = ret * 10 + ch - '0', ch = getchar();
	return sgn ? -ret : ret;
}
const int N = 180, mod = 998244353;
const ll INF = 1e18;
int n, q;
ll a[N], sum[N];
map<ll, int> mp[N];

int dfs(ll x, int cur) {
	if (x < 0 || x > sum[cur]) return 0;
	if (!cur) return (x == 0);
	if (mp[cur].count(x)) return mp[cur][x];
	return mp[cur][x] = (dfs(x - a[cur], cur - 1) + dfs(x, cur - 1)) % mod;
}

int main() {
	int T = read();
	while (T--) {
		n = read(), q = read();
		for (int i = 1; i <= n; i++) a[i] = read();
		int m = n;
		while (a[m] <= INF) {
			a[++m] = 0;
			for (int i = 1; i <= n; i++) a[m] += a[m - i];
		}
		n = m - 1;
		for (int i = 1; i <= n; i++) {
			sum[i] = sum[i - 1] + a[i];
			mp[i].clear();
		}
		while (q--) {
			ll x = read();
			printf("%d\n", dfs(x, n));
		}
	}
	return 0;
}
6 ABC298H Sum of Min of Length
Problem

给定一棵 n 个节点的树。m 次询问,每次询问给出 L,R,求 i=1nmin(d(i,L),d(i,R))d(i,j) bi表示点 i 到点 j 的距离。

1n,m2×1051L,Rn

3s1GB

Sol

因为是求 i=1nmin{d(i,L),d(i,R)} 的值,考虑讨论 d(i,L)d(i,R) 的大小。

p=LCA(L,R)depL>depR,dist=depL+depR2×deppnow 满足 depLdepnow=dist2

L 一定在 now 的子树内,且对于 isubtreenow 时均有 d(i,L)d(i,R),否则 d(i,L)>d(i,R)。其中 subtreex 表示 x 的子树。

容易想到求一个点到其他点的距离和。

vali 表示 j=1nd(i,j)

在 dfs 时处理一下即可,显然可以做到 O(n)

最后把距离分奇偶讨论一下即可。

时间复杂度:O(n+(n+m)logn)

Code
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
const int inf = 2147483647, mod = 1011451423;
const ll INF = 1e18;

const int N = 2e5 + 10;
int n, m;
int siz[N], d[N], fa[N][20];
ll dis[N], v[N];
vector<int> e[N];

void dfs1(int u, int f) {
	d[u] = d[f] + 1, siz[u] = 1, fa[u][0] = f;
	for (int i = 1; i <= 18; i++)
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	for (int v : e[u])
		if (v != f) {
			dfs1(v, u);
			siz[u] += siz[v];
			dis[u] += dis[v] + siz[v];
		}
}
void dfs2(int u, int f) {
	if (u == 1)
		v[u] = dis[u];
	else
		v[u] = v[f] + (siz[1] - siz[u]) - siz[u];
	for (int v : e[u])
		if (v != f)
			dfs2(v, u);
}
int lca(int u, int v) {
	if (d[u] < d[v])
		swap(u, v);
	for (int i = 18; i >= 0; i--)
		if (d[fa[u][i]] >= d[v])
			u = fa[u][i];
	if (u == v)
		return u;
	for (int i = 18; i >= 0; i--)
		if (fa[u][i] != fa[v][i])
			u = fa[u][i], v = fa[v][i];
	return fa[u][0];
}
int tonode(int u, int st) {
	for (int i = 18; i >= 0; i--)
		if (st >> i & 1)
			u = fa[u][i];
	return u;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin >> n;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].pb(v), e[v].pb(u);
	}
	dfs1(1, 0), dfs2(1, 0);
	cin >> m;
	while (m--) {
		int l, r;
		cin >> l >> r;
		if (d[l] < d[r])
			swap(l, r);
		int p = lca(l, r), dist = d[l] + d[r] - 2 * d[p];
		if (dist % 2 == 0) {
			int now = tonode(l, dist / 2);
			cout << v[l] + v[r] - v[now] - 1ll * n * dist / 2 << "\n";
		} else {
			int now = tonode(l, dist / 2);
			cout << v[l] + v[r] - v[now] - 1ll * n * (dist / 2) - siz[now] << "\n";
		}
	}
	return 0;
}
7 P7834 [ONTAK2010] Peaks 加强版
Problem

给定一张 n 个点、m 条边的无向图,第 i 个点的权值为 ai,边有边权。

q 组询问,每组询问给定三个整数 u,x,k,求从 u 开始只经过权值 x 的边所能到达的权值第 k 大的点的权值,如果不存在输出 1,强制在线。

1n1050m,q5×1051s,tn1ai,w1090u,x,k<231

2.5s128MB

Sol

发现有只经过权值 x 的边的限制,容易想到 Kruskal 重构树,因为 Kruskal 重构树可以轻松求出从 u 出发只经过不超过 x 的边的点集,这是一个常见的 trick。然后每次询问就是问一个子树中的第 k 大的点是多少,这个东西暴力线段树合并,也可以求出 dfs 序,然后用可持久化线段树维护。注意线段树合并也需要可持久化,所以空间常数可能较大。

下面的代码是可持久化线段树合并写法 by Alex_Wei。

Code
#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
using namespace std;
using namespace __gnu_pbds;
#define fi first
#define se second
#define TIME 1e3 * clock() / CLOCKS_PER_SEC
using ll = long long;
using uint = unsigned int;
using ld = long double;
// using lll = __int128;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using ull = unsigned long long;
inline ll read() {
	ll x = 0, sgn = 0;
	char s = getchar();
	while(!isdigit(s)) sgn |= s == '-', s = getchar();
	while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
	return sgn ? -x : x;
}
inline void print(ll x) {
	if(x < 0) return putchar('-'), print(-x);
	if(x >= 10) print(x / 10);
	putchar(x % 10 + '0');
}
bool Mbe;

constexpr int N = 1e5 + 5;
constexpr int M = 5e5 + 5;
constexpr int V = N << 1;
constexpr int K = N * 40;

// linklist
int cnt, hd[V], nxt[V], to[V];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}

// dsu
int fa[V];
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}

struct SegTree {
	int node, ls[K], rs[K], val[K];
	void modify(int l, int r, int p, int &x) {
		if(!x) x = ++node;
		val[x]++;
		if(l == r) return;
		int m = l + r >> 1;
		if(p <= m) modify(l, m, p, ls[x]);
		else modify(m + 1, r, p, rs[x]);
	}
	int merge(int x, int y) {
		if(!x || !y) return x | y;
		int z = ++node;
		ls[z] = merge(ls[x], ls[y]);
		rs[z] = merge(rs[x], rs[y]);
		return val[z] = val[x] + val[y], z;
	}
	int binary(int l, int r, int x, int k) {
		if(l == r) return l;
		int m = l + r >> 1;
		if(val[rs[x]] >= k) return binary(m + 1, r, rs[x], k);
		return binary(l, m, ls[x], k - val[rs[x]]);
	}
} tr;

// basic
int n, m, q, a[N], d[N];

// kruskal
int node, lg, val[V], R[V], anc[18][V];
struct edge {
	int u, v, w;
	bool operator < (const edge &x) const {return w < x.w;}
} e[M];
void dfs(int id) {
	if(id <= n) tr.modify(1, n, a[id], R[id]);
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		anc[0][it] = id, dfs(it);
		R[id] = tr.merge(R[id], R[it]);
	}
}

bool Med;
int main() {
	fprintf(stderr, "%.3lf MB\n", (&Mbe - &Med) / 1048576.0);
	#ifdef ALEX_WEI
		FILE* IN = freopen("1.in", "r", stdin);
		FILE* OUT = freopen("1.out", "w", stdout);
	#endif
	cin >> n >> m >> q;
	for(int i = 1; i <= n; i++) d[i] = a[i] = read();
	sort(d + 1, d + n + 1);
	for(int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + n + 1, a[i]) - d;
	for(int i = 1; i <= m; i++) e[i].u = read(), e[i].v = read(), e[i].w = read();
	sort(e + 1, e + m + 1), node = n;
	for(int i = 1; i <= n + n; i++) fa[i] = i;
	for(int i = 1; i <= m; i++) {
		int u = find(e[i].u), v = find(e[i].v);
		if(u == v) continue;
		val[++node] = e[i].w, fa[u] = fa[v] = node;
		add(node, u), add(node, v);
	}
	 lg = 31 - __builtin_clz(node), dfs(node);
	 for(int i = 1; i <= lg; i++)
		 for(int j = 1; j <= node; j++)
			 anc[i][j] = anc[i - 1][anc[i - 1][j]];
	 for(int _ = 1, lst = 0; _ <= q; _++) {
		 int u = (read() ^ lst) % n + 1, x = read() ^ lst;
		 int k = (read() ^ lst) % n + 1;
		 for(int i = lg; ~i; i--) {
			 int v = anc[i][u];
			 if(v && val[v] <= x) u = v;
		 }
		 if(tr.val[R[u]] < k) print(-1), lst = 0;
		 else print(lst = d[tr.binary(1, n, R[u], k)]);
		 putchar('\n');
	 }
	cerr << TIME << " ms\n";
	return 0;
}
8 P5313 [Ynoi2011] WBLT
Problem

给定一个长为 n 的序列,有 m 次查询,每次查询给定参数 l,r,b,求找到最大的 x 使得存在 a[0,b)b,2b,,a+(x1)b 均出现在 [l,r] 中。

保证所有数均在 [0,105] 之间。

1.5s128MB

Sol

看到支持离线,可以想到莫队。

先想一下暴力是怎么做的:每次枚举 a,每次加 b,看什么时候 buca+kb=0

考虑怎么转化到莫队上去,莫队左右端点 ±1 的复杂度是 O(1) 的,想一下怎么优化查询。发现如果对于每个令 arrk=buca+kb,然后发现这是一个类似于 mex 的东西,即值是第一个 0 的位置,可以写成前缀与一直到 0

考虑先将桶按 b 分段,将每段分别进行按位与运算,做完第 i 段时用于运算的桶全都为 0,就可以直接得到答案。这显然可以用 bitset 优化。但是 STL 的 bitset 不支持分裂操作,所以需要手写。

b 较大时时间复杂度为 O(nm+mVb×bw)(不考虑排序)。

但是发现当 b<w 时时间复杂度是错的,此时就变为了 O(nm+mVb),因为这时候小段太短了,bitset 的压位就失去了作用。

现在考虑 b<64 的情况。

由于 b 很小,考虑对于每一个 b[1,w1]) 做一遍莫队。

这时就只需在 modb 的位置加 1,即维护 b 剩余类。又发现 modb 的值较小,直接对每一个 i 维护其 mex 即可,最后的答案就是 max{mex},对于每一个 imex,依然可以用 bitset 优化。

这一部分的时间复杂度为 O(i=1w1(ncntqi+iVi×1w)),由均值不等式 i=1naini=1nai2n 可得这部分的时间复杂度上界为 O(n×w(cntqi)2w+V),即 O(nmw+V),其中的 w 为常数。

时间复杂度:O(nm+mVw)

代码好像有 UB /gg。

Code
#include<bits/stdc++.h>
#define pb push_back
#define ios ios::sync_with_stdio(0); cin.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 2147483647, mod = 1e9 + 7;
const ll INF = 1e18;

const int N = 1e5 + 10, V = 1e5 + 1, MOD = 63, W = 64, bit = 6;
int n, m, len, cq, nowb;
int a[N], cnt[N], ans[N];
ull filter[W];
struct que {
	int l, r, b, id;
} q[N];
bool cmp(que a, que b) {
	int x = a.l / len, y = b.l / len;
	if (x == y) {
		if (x & 1) return a.r < b.r;
		return a.r > b.r;
	}
	return a.l < b.l;
}
vector<que> qb[W];

struct bitsett {
  int siz;
	vector<ull> v;
	void reset() { for (int i = 0; i <= siz; i++) v[i] = 0; }
	void set() { for (int i = 0; i <= siz; i++) v[i] = ~0ull; }
	void set1(int x) { v[x >> bit] |= (1ull << (x & MOD)); }
	void set0(int x) { v[x >> bit] &= (~(1ull << (x & MOD))); }
	void flip() { for (int i = 0; i <= siz; i++) v[i] = ~v[i]; }
	void operator &= (const bitsett &a) { for (int i = 0; i <= siz; i++) v[i] &= a.v[i]; }
	int mex() { for (int i = 0; ; i++) if (~v[i]) return i << bit | __builtin_ctzll(~v[i]); }
	void init(int x) {
		v.resize((x >> bit) + 2);
		siz = x >> bit;
		reset();
	}
	bool any() {
		for (int i = 0; i <= siz; i++) if (v[i]) return 1;
		return 0;
	}
};
bitsett cur, res, bl[1605], blb[W + 5];
void split(int len) {
	for (int i = 0; i <= V / len + 2; i++) bl[i].init(len);
	int ned = bl[0].siz, bg = 0, np = 0, num = 0;
	for (int i = 0; i <= cur.siz; i++)
		if (np == ned) {
			if (bg + (len & MOD) <= MOD) {
				bl[num].v[np] = (cur.v[i] & (filter[bg + (len & MOD) - 1] - (bg ? filter[bg - 1] : 0))) >> bg;
				i--;
			}
			else if (i != cur.siz) {
				bl[num].v[np] = (cur.v[i] >> bg) | ((cur.v[i + 1] & filter[bg + (len & MOD) - MOD]) << (W - bg));
			} else
				bl[num].v[np] = (cur.v[i] >> bg);
			bg = (bg + (len & MOD)) & MOD, np = 0, num++;
		} else {
			if (bg == 0) bl[num].v[np] = cur.v[i];
			else if (i != cur.siz) bl[num].v[np] = (cur.v[i] >> bg) | ((cur.v[i + 1] & filter[bg - 1]) << (W - bg));
			else bl[num].v[np] = cur.v[i] >> bg;
			np++;
		}
}
void add1(int x) {
	cnt[x]++;
	if (cnt[x] == 1) cur.set1(x);
}
void del1(int x) {
	cnt[x]--;
	if (cnt[x] == 0) cur.set0(x);
}
void add2(int x) {
	cnt[x]++;
	if (cnt[x] == 1) blb[x % nowb].set1(x / nowb);
}
void del2(int x) {
	cnt[x]--;
	if (cnt[x] == 0) blb[x % nowb].set0(x / nowb);
}

int main() {
	filter[0] = 1;
	for (int i = 1; i < W; i++) filter[i] = filter[i - 1] + (1ull << i);
	cin >> n; 
	for (int i = 1; i <= n; i++) cin >> a[i];
	cin >> m;
	for (int i = 1, l, r, b; i <= m; i++) {
		cin >> l >> r >> b;
		if (b > 63) q[++cq] = (que){l, r, b, i};
		else qb[b].pb((que){l, r, 0, i});
	}
	len = n / sqrt(cq) + 1;
	sort(q + 1, q + cq + 1, cmp);
	cur.init(V);
	for (int i = 1, l = 1, r = 0; i <= cq; i++) {
		while (l > q[i].l) add1(a[--l]);
		while (r < q[i].r) add1(a[++r]);
		while (l < q[i].l) del1(a[l++]);
		while (r > q[i].r) del1(a[r--]);
		split(q[i].b);
		res.init(q[i].b);
		res.flip();
		for (int j = 0; ; j++) {
			res &= bl[j];
			if (!res.any()) {
				ans[q[i].id] = j;
				break;
			}
		}
	}
	int mxb = 63;
	for (int i = 1; i <= mxb; i++) {
		if (qb[i].size() == 0) continue;
		memset(cnt, 0, sizeof(cnt));
		nowb = i;
		len = n / sqrt(qb[i].size()) + 1;
		sort(qb[i].begin(), qb[i].end(), cmp);
		for (int j = 0; j < i; j++) blb[j].init(V / i + 1);
		for (int j = 0, l = 1, r = 0; j < (int)(qb[i].size()); j++) {
			while (l > qb[i][j].l) add2(a[--l]);
			while (r < qb[i][j].r) add2(a[++r]);
			while (l < qb[i][j].l) del2(a[l++]);
			while (r > qb[i][j].r) del2(a[r--]);
			for (int k = 0; k < i; k++)
				ans[qb[i][j].id] = max(ans[qb[i][j].id], blb[k].mex());
		}
	}
	for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
	return 0;
}
9 P9410 『STA - R2』机场修建
Problem

n 个城市排成一列,最开始是互不连通的,每个城市初始都没有人口。
会出现以下操作 / 查询共 m 个:

  1. 1 x y 开通城市 x 和城市 y 之间的双向航班。
  2. 2 l r a 城市 [l,r] 的人口数都 +a
  3. 3 x 如果所有能够到达城市 x 的人都来到城市 x,城市 x 有多少人。

1n,m2×1050a109。保证答案在 64 位有符号整形表示的范围内。

1s125MB

Sol

感觉这种序列上各个部分不一定连续的查询以及区间加,对 x,y 分别增加一个查询点这种操作很适合分块 / 根号分治。

下面是分块的题解(当然根号分治也可以做):

考虑稍微暴力的分块。区间加的话,散块部分可以直接加到全局的 sum 数组中,毕竟不是区间求和,然后整块部分直接打标记,记录块内每个连通块的大小即可。然后合并就是对于每一个块分别合并就行。但这样空间是 O(nn) 的,考虑优化。

占内存的是要维护每一块的 sz 数组,发现 sz 数组会占用很多无用的内存。因为 sz 总数为 O(n),对每一个颜色存储在每一块的数量,这里因为没有出现的不会影响答案,可以不记录,因此使用 vector 动态扩容即可。具体地,维护每个连通块出现的块的编号和它在该块内的元素数量。这样就做到了空间线性。

时间复杂度:O(nn)。空间线性。

Code
// Problem:P9410
#include<bits/stdc++.h>
#define pb push_back
#define ios ios::sync_with_stdio(0); cin.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9, infi32 = 2147483647, mod = 1e9 + 7;
const ll INF = 1e18;

const int N = 2e5 + 10, B = 450 + 10;
int n, m, len, b;
int fa[N], sz[N], L[B], R[B], pos[N];
ll tg[B], sum[N], tmp[B];

vector<pair<int, ll> > v[N];

int find(int x) {
	if (fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}

int main() {
//	freopen("data.in", "r", stdin);
//	freopen("myans.out", "w", stdout);
	ios
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		fa[i] = i;
		sz[i] = 1;
	}
	len = sqrt(n), b = n / len + (n % len != 0);
	for (int i = 1; i <= b; i++) {
		L[i] = (i - 1) * len + 1, R[i] = min(i * len, n);
		for (int j = L[i]; j <= R[i]; j++) pos[j] = i;
	}
	for (int i = 1; i <= n; i++) v[i].pb(make_pair(pos[i], 1));
	for (int t = 1, opt, x, y, z; t <= m; t++) {
		cin >> opt >> x;
		if (opt == 1) {
			cin >> y;
			x = find(x), y = find(y);
			if (x == y) continue;
			memset(tmp, 0, sizeof(tmp));
			if (sz[x] > sz[y]) swap(x, y);
			fa[x] = y, sz[y] += sz[x], sum[y] += sum[x];
			for (int i = 0; i < v[x].size(); i++) tmp[v[x][i].first] += v[x][i].second;
			for (int i = 0; i < v[y].size(); i++) tmp[v[y][i].first] += v[y][i].second;
			v[x].clear(); v[y].clear();
			for (int i = 1; i <= b; i++)
				if (tmp[i])
					v[y].pb(make_pair(i, tmp[i]));
		} else if (opt == 2) {
			cin >> y >> z;
			int p = pos[x], q = pos[y];
			if (p == q) {
				for (int i = x; i <= y; i++) sum[find(i)] += z;
				continue;
			}
			for (int i = x; i <= R[p]; i++) sum[find(i)] += z;
			for (int i = L[q]; i <= y; i++) sum[find(i)] += z;
			for (int i = p + 1; i < q; i++) tg[i] += z;
		} else {
			x = find(x);
			ll res = sum[x];
			for (int i = 0; i < v[x].size(); i++)
				res += tg[v[x][i].first] * v[x][i].second;
			cout << res << "\n";
		}
	}
	return 0;
}
10 P7735 NOI2021 轻重边
Problem

给定一棵 n 个点的树,树上的边有黑白两种颜色,初始时所有点都是白色。要求支持 m 次操作,操作分为两种:

  1. 给定两个点 u,v,对于 u,v 路径上的所有点 x,将与其相连的边变为白色,然后将路径 u,v 染黑。
  2. 给定两个点 u,v,求 u,v 路径经过的黑色边的数量。

n105m105

1s1GB

Sol

还是有点智慧的?

发现如果要正常树剖(即在实质上更改每一条边的颜色,如用数据结构维护)的话,是需要同时维护 BFS 序和 DFS 序,这是无法做到的。考虑将边的颜色进行一个转化,即满足另外的某种条件使得可以判断边的颜色,且要能够方便修改与查询。

发现其实边的修改是很麻烦的,操作修改的都是边,而边又依赖于点,考虑以点的条件来判断边的颜色。那么边的颜色显然会取决于两边的点。如果只要一端(如钦定深度较小的点)是有问题的,因为 u 可能有多个儿子,就没法判断了。发现是对于 x,其所有出边均会染为白色(在路径上的除外),那么不妨给每个点赋一个权重 wx,当边的两端 u,v 的权重相等时,边才为黑色。那修改就只需要令 xpath(u,v),wxvalval 为一个之前没有出现过的权值。统计显然可以用线段树解决(这个如果不会可以参考 P2486)。

于是这道题就在 O(nlog2n) 的时间复杂度内完成了。

Code
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 2147483647, mod = 1e9 + 7;
const ll INF = 1e18;

const int N = 1e5 + 10;
int n, m, cnt, numc;
int son[N], fa[N], dfn[N], rdfn[N], top[N], siz[N], d[N];
int head[N], cnte;
struct edge {
	int v, nxt;
} e[N << 1];
void adde(int u, int v) {
	e[++cnte].v = v;
	e[cnte].nxt = head[u];
	head[u] = cnte;
}
struct node {
	int s, lc, rc, tg;
	friend bool operator != (node a, node b) {
		return a.s != b.s || a.lc != b.lc || a.rc != b.rc || a.tg != b.tg;
	}
	friend bool operator == (node a, node b) {
		return !(a != b);
	}
} t[N << 2], unt;
struct segt {
	void pushup(int u) {
		t[u].s = t[u << 1].s + t[u << 1 | 1].s;
		if (t[u << 1].rc == t[u << 1 | 1].lc) t[u].s++;
		t[u].lc = t[u << 1].lc, t[u].rc = t[u << 1 | 1].rc;
	}
	void pushdown(int u, int l, int r) {
		if (t[u].tg) {
			int mid = l + r >> 1;
			t[u << 1].tg = t[u << 1 | 1].tg = t[u].tg;
			t[u << 1].lc = t[u << 1].rc = t[u << 1 | 1].lc = t[u << 1 | 1].rc = t[u].tg;
			t[u << 1].s = mid - l, t[u << 1 | 1].s = r - mid - 1;
			t[u].tg = 0;
		}
	}
	void build(int u, int l, int r) {
		if (l == r) {
			t[u].lc = t[u].rc = ++numc;
			t[u].s = 0;
			return;
		}
		int mid = l + r >> 1;
		build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
	void update(int u, int l, int r, int L, int R, int col) {
		if (L <= l && r <= R) {
			t[u].tg = t[u].lc = t[u].rc = col;
			t[u].s = r - l;
			return;
		}
		pushdown(u, l, r);
		int mid = l + r >> 1;
		if (L <= mid) update(u << 1, l, mid, L, R, col);
		if (mid < R) update(u << 1 | 1, mid + 1, r, L, R, col);
		pushup(u);
	}
	node query(int u, int l, int r, int L, int R) {
		if (L <= l && r <= R) return (node){t[u].s, t[u].lc, t[u].rc, 0};
		pushdown(u, l, r);
		int mid = l + r >> 1;
		node ax = unt, ay = unt, res = unt;
		if (L <= mid) ax = query(u << 1, l, mid, L, R);
		if (mid < R) ay = query(u << 1 | 1, mid + 1, r, L, R);
		if (ax == unt) return ay;
		else if (ay == unt) return ax;
		res.lc = ax.lc, res.rc = ay.rc;
		res.s = ax.s + ay.s + (ax.rc == ay.lc);
		return res;
	}
} tr;

void dfs1(int u, int f) {
	siz[u] = 1, fa[u] = f, d[u] = d[f] + 1;
	int maxp = 0;
	for (int i = head[u], v; i != 0; i = e[i].nxt)
		if ((v = e[i].v) != f) {
			dfs1(v, u);
			siz[u] += siz[v];
			if (siz[maxp] < siz[v]) maxp = v;
		}
	son[u] = maxp;
}
void dfs2(int u, int f) {
	top[u] = f, dfn[u] = ++cnt, rdfn[cnt] = u;
	if (!son[u]) return;
	dfs2(son[u], f);
	for (int i = head[u], v; i != 0; i = e[i].nxt)
		if ((v = e[i].v) != fa[u] && v != son[u])
			dfs2(v, v);
}
void upd(int u, int v, int col) {
	while (top[u] != top[v]) {
		if (d[top[u]] < d[top[v]]) swap(u, v);
		tr.update(1, 1, n, dfn[top[u]], dfn[u], col);
		u = fa[top[u]];
	}
	if (d[u] < d[v]) swap(u, v);
	tr.update(1, 1, n, dfn[v], dfn[u], col);
}
int getlca(int u, int v) {
	while (top[u] != top[v]) {
		if (d[top[u]] < d[top[v]]) swap(u, v);
		u = fa[top[u]];
	}
	if (d[u] < d[v]) swap(u, v);
	return v;
} 
int qry(int u, int v) {
	int lcl = -1, res = 0, lca = getlca(u, v); node p;
	while (top[u] != top[lca]) {
		p = tr.query(1, 1, n, dfn[top[u]], dfn[u]);
		res += p.s + (p.rc == lcl);
		lcl = p.lc;
		u = fa[top[u]];
	}
	p = tr.query(1, 1, n, dfn[lca], dfn[u]);
	res += p.s + (p.rc == lcl);
	lcl = -1;
	while (top[v] != top[lca]) {
		p = tr.query(1, 1, n, dfn[top[v]], dfn[v]);
		res += p.s + (p.rc == lcl);
		lcl = p.lc;
		v = fa[top[v]];
	}
	p = tr.query(1, 1, n, dfn[lca], dfn[v]);
	res += p.s + (p.rc == lcl);
	return res;
}
void clr() {
	memset(t, 0, sizeof(t));
	memset(head, 0, sizeof(head));
	memset(son, 0, sizeof(son));
	memset(fa, 0, sizeof(fa));
	memset(d, 0, sizeof(d));
	memset(siz, 0, sizeof(siz));
	memset(dfn, 0, sizeof(dfn));
	memset(rdfn, 0, sizeof(rdfn));
	memset(top, 0, sizeof(top));
	memset(e, 0, sizeof(e));
	cnte = cnt = numc = 0;
	unt = (node){0, 0, 0, 0};
}

void solve() {
	clr();
	cin >> n >> m;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		adde(u, v), adde(v, u);
	}
	dfs1(1, 0);
	dfs2(1, 1);
	tr.build(1, 1, n);
	for (int i = 1, opt, u, v; i <= m; i++) {
		cin >> opt >> u >> v;
		if (opt == 1)
			upd(u, v, ++numc);
		else
			cout << qry(u, v) << "\n";
	}
}

int main() {
	ios :: sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	int T;
	cin >> T;
	while (T--)
		solve();
	return 0;
}
11 P4689 [Ynoi2016] 这是我自己的发明
Problem

给定一棵 n 个点的树,初始时的根节点为 1。要求支持 m 个操作,种类如下:

  • 1 x,将根节点换为 x
  • 2 x y,给定两个点 x,y,求从 x 的子树中选一个点,y 的子树中选一个点,点权相等的情况数。

n105m5×105ai109

1.5s512MB

Sol

首先换根操作肯定是假的,然后这个时候 x,y 的子树在 dfn 上肯定是连续的一段/两段。分别记这四个区间为 a,b,c,df([l,r],[l,r]) 为从 [l,r][l,r] 中分别选一个数,权相等的情况数,那么显然有 f(ab,cd)=f(a,c)+f(a,d)+f(b,c)+f(b,d)。然后有 f([l,r],[l,r])=f([1,r],[1,r])f([1,l1],[1,r1])f([1,l1],[1,r])+f([1,l1],[1,r1])。所有就只需要考虑前缀了。记 g(i,j)=f([1,i],[1,j])。若将 g 看作单点查询,则最多有 O(m)次查询,发现这个东西显然可以莫队解决,于是就完了。

实现上可以将前面最开始的 4 个区间可以通过将维护的区间范围由 [1,n] 变为 [1,2n] 更加方便。

时间复杂度:O(mn)

Code
// Problem:P4689
#include<bits/stdc++.h>
#define pb push_back
#define ios ios::sync_with_stdio(0); cin.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 2147483647, mod = 1e9 + 7;
const ll INF = 1e18;

const int N = 1e5 + 10, QN = 5e5 + 10;
int n, m, num, rt, cnt, cq, len, queq;
int a[N], b[N], t[N], dfn[N], rdfn[N << 1], siz[N], c1[N], c2[N], fa[N][20], d[N];
ll res, ans[QN];
int head[N], cnte;
struct edge {
	int v, nxt;
} e[N << 1];
void adde(int u, int v) {
	e[++cnte].v = v;
	e[cnte].nxt = head[u];
	head[u] = cnte;
}
struct que {
	int l, r, typ, id;
} q[QN << 2];
bool cmp(que a, que b) {
	int x = a.l / len, y = b.l / len;
	if (x == y) {
		if (x & 1) return a.r < b.r;
		return a.r > b.r;
	}
	return a.l < b.l;
}

void dfs_init(int u, int f) {
	dfn[u] = ++cnt, rdfn[cnt] = u, siz[u] = 1, fa[u][0] = f, d[u] = d[f] + 1;
	for (int i = 1; i <= 18; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
	for (int i = head[u], v; i != 0; i = e[i].nxt)
		if ((v = e[i].v) != f) {
			dfs_init(v, u);
			siz[u] += siz[v];
		}
}
int getnode(int u, int v) {
	for (int i = 18; i >= 0; i--)
		if (d[fa[u][i]] > d[v])
			u = fa[u][i];
	return u;
}
void add1(int x) { c1[x]++; res += c2[x]; }
void del1(int x) { c1[x]--; res -= c2[x]; }
void add2(int x) { c2[x]++; res += c1[x]; }
void del2(int x) { c2[x]--; res -= c1[x]; }

int main() {
//	freopen("data.in", "r", stdin);
//	freopen("myans.out", "w", stdout);
	ios
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		b[i] = a[i];
	}
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		adde(u, v), adde(v, u);
	}
	dfs_init(rt = 1, 0);
	sort(b + 1, b + n + 1);
	num = unique(b + 1, b + n + 1) - b - 1;
	for (int i = 1; i <= n; i++) {
		a[i] = lower_bound(b + 1, b + num + 1, a[i]) - b;  
		rdfn[n + i] = rdfn[i];
	}
	for (int i = 1, opt, x, y; i <= m; i++) {
		cin >> opt >> x;
		if (opt == 1) rt = x;
		else {
			cin >> y; queq++;
			int a, b, c, d;
			if (rt == x) a = 1, b = n;
			else if (dfn[x] < dfn[rt] && dfn[rt] <= dfn[x] + siz[x] - 1) {
				int pos = getnode(rt, x);
				a = dfn[pos] + siz[pos], b = n + dfn[pos] - 1;
			} else a = dfn[x], b = dfn[x] + siz[x] - 1;
			if (rt == y) c = 1, d = n;
			else if (dfn[y] < dfn[rt] && dfn[rt] <= dfn[y] + siz[y] - 1) {
				int pos = getnode(rt, y);
				c = dfn[pos] + siz[pos], d = n + dfn[pos] - 1;
			} else c = dfn[y], d = dfn[y] + siz[y] - 1;
			if (a > c) swap(a, c), swap(b, d);
			q[++cq] = (que){min(b, d), max(b, d), 1, queq};
			q[++cq] = (que){min(a - 1, c - 1), max(a - 1, c - 1), 1, queq};
			q[++cq] = (que){min(b, c - 1), max(b, c - 1), -1, queq};
			q[++cq] = (que){min(d, a - 1), max(d, a - 1), -1, queq};
		}
	}
	len = 2 * n / sqrt(cq) + 1;
	sort(q + 1, q + cq + 1, cmp);
	for (int i = 1, l = 0, r = 0; i <= cq; i++) {
		while (l < q[i].l) add1(a[rdfn[++l]]);
		while (r < q[i].r) add2(a[rdfn[++r]]);
		while (l > q[i].l) del1(a[rdfn[l--]]);
		while (r > q[i].r) del2(a[rdfn[r--]]);
		ans[q[i].id] += q[i].typ * res;
	}
	for (int i = 1; i <= queq; i++) cout << ans[i] << "\n";
	return 0;
}
posted @   Pengzt  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示