2024.9 - 做题记录与方法总结

CSP-S 的丧钟为谁而鸣?它正是为你而鸣! ——《CSP-S为谁而鸣》


2024/09/06#

拖到今天才写,主要是开学了

CF609E Minimum spanning tree for each edge#

题面:

题面翻译
题目描述

给你 \(n\) 个点,\(m\) 条边,如果对于一个最小生成树中要求必须包括第 \(i(1 \le i \le m)\) 条边,那么最小生成树的权值总和最小是多少。

输入格式

第一行有两个整数 \(n\)\(m\)

接下来 \(m\) 行,每行有三个整数 \(u\)\(v\)\(w\) 表示一条权值为 \(w\) 的边连接 \(u\)\(v\)

输出格式

总共 \(m\) 行,第 \(i\) 行一个整数代表包括第 \(i\) 条边时的最小权值和。

说明/提示

\(1 \le n \le 2 \times 10^5,n-1 \le m\le 2 \times 10^5\)

\(1 \le u_i,v_i \le n,u_i \neq v_i,1 \le w_i \le 10^9\)

样例 #1
样例输入 #1
5 7
1 2 3
1 3 1
1 4 5
2 3 2
2 5 3
3 4 2
4 5 4
样例输出 #1
9
8
11
8
8
8
9

这道题注意考察经典生成树结论: 次小生成树只会从原最小生成树替换掉一条边,替换掉两条边肯定不优!

一个简短的证明:

不难注意到,用 Kruskal 算法求最小生成树时,我们 放弃掉两条最小生成树边,用其他两条权值去替换只放弃掉一条边,用其他一条边替换 ,两种决策显然第二种更优!

那么我们强制使用一条边,先看他是不是在最小生成树上?如果是,直接输出最小生成树权值。

如果不是,我们看如何替换。

当我们求出最小生成树后,再添加一条边,必然出现环,此时就是基环树

我们都知道,只能在环上断边才能重新变成树,所以问题变成了环上断边。

因为我们要让最小生成树权值最小,我们肯定断环上权值最大的边,考虑树剖。

我们用树剖求出强制边 \((u,v)\) 所在环上最大值:因为 \((u,v)\) 强制,实际上是 \(path : (u,v)\) 上的边权最大值。

所以 sol 就是 Kruskal + 树剖(边权转点权)

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long	
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int M = 2e5+5,N = M,inf = 0x3f3f3f3f3f3f3f3fLL;
int n,m,ans,det[M],s[N];
array<int,4> e[M];
bool vis[N];
int head[N],nxt[N<<1],to[N<<1],cnt,val[N<<1];
void init() {memset(head,-1,sizeof(head));cnt = 0;}
void add(int u,int v,int w) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	val[cnt] = w;
	head[u] = cnt++;
}
int find(int x) {
	if(s[x] ^ x) s[x] = find(s[x]);
	return s[x];
}

void kruskal() {
	for(int i = 1;i<=n;i++) s[i] = i;
	int tot = 0;
	for(int i = 1;i<=m;i++) {
		int fx = find(e[i][1]),fy = find(e[i][2]);
		if(fx ^ fy) {
			s[fx] = fy;
			vis[e[i][3]] = true;
			add(e[i][1],e[i][2],e[i][0]);
			add(e[i][2],e[i][1],e[i][0]);
			ans += e[i][0];
			tot++;
			if(tot == n - 1) break;
		}
	}
}

int dep[N],fa[N],siz[N],son[N],id[N],w[N],_w[N],top[N],num;

void dfs1(int x,int f) {
	fa[x] = f;
	siz[x] = 1;
	dep[x] = dep[f] + 1;
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f) {
			dfs1(y,x);
			w[y] = val[i];
			siz[x] += siz[y];
			if(siz[son[x]] < siz[y]) son[x] = y;
		}
	}
}

void dfs2(int x,int topx) {
	top[x] = topx;
	id[x] = ++num;
	_w[num] = w[x];
	if(!son[x]) return;
	dfs2(son[x],topx);
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ fa[x] && y ^ son[x]) dfs2(y,y);
	}
}

namespace sgt{
#define ls (p << 1)
#define rs (ls | 1)
#define mid ((pl + pr) >> 1)

int mx[N<<2];

void push_up(int p) {
	mx[p] = max(mx[ls],mx[rs]);
}

void build(int p,int pl,int pr) {
	if(pl == pr) {
		mx[p] = _w[pl];
		return;
	}
	build(ls,pl,mid);
	build(rs,mid+1,pr);
	push_up(p);
}

int query(int p,int pl,int pr,int l,int r) {
	if(l > r) return 0;
	if(l <= pl && pr <= r) return mx[p];
	if(r <= mid) return query(ls,pl,mid,l,r);
	else if(l > mid) return query(rs,mid+1,pr,l,r);
	else return max(query(ls,pl,mid,l,r),query(rs,mid+1,pr,l,r));
}

}

int query(int x,int y) {
	int re = 0;
	while(top[x] ^ top[y]) {
		if(dep[top[x]] < dep[top[y]]) swap(x,y);
		re = max(re,sgt::query(1,1,n,id[top[x]],id[x]));
		x = fa[top[x]];
	}
	if(dep[x] > dep[y]) swap(x,y);
	re = max(re,sgt::query(1,1,n,id[x] + 1,id[y]));
	return re;
}
int a[N];

signed main() {
	init();
	n = rd(),m = rd();
	for(int i = 1;i<=m;i++) 
		e[i][1] = rd(),e[i][2] = rd(),e[i][0] = rd(),e[i][3] = i;
	sort(e + 1,e + m + 1);
	kruskal();
	dfs1(1,0),dfs2(1,1);
	sgt::build(1,1,n);
	for(int i = 1;i<=m;i++) {
		if(vis[e[i][3]]) a[e[i][3]] = ans;
		else  {
			int re = query(e[i][1],e[i][2]);
			a[e[i][3]] = ans + e[i][0] - re; 
		}
	}
	for(int i = 1;i<=m;i++) wt(a[i]),putchar('\n');
	
	return 0;
}

CF1009F Dominant Indices#

题面:

题目描述

你得到一个包含 \(n\) 个顶点的无向树,顶点 \(1\) 是树的根。

我们将顶点 \(x\) 的深度数组定义为一个无限序列 \([d_{x, 0}, d_{x, 1}, d_{x, 2}, \dots]\),其中 \(d_{x, i}\) 是满足以下两个条件的顶点 \(y\) 的数量:

\(x\)\(y\) 的祖先;
\(x\)\(y\) 的简单路径正好经过 \(i\) 条边。
顶点 \(x\) 的深度数组的主导索引(简称为顶点 \(x\) 的主导索引)是一个索引 \(j\),满足:

对于每个 \(k < j\)\(d_{x, k} < d_{x, j}\)
对于每个 \(k > j\)\(d_{x, k} \le d_{x, j}\)
对于树中的每个顶点,计算其主导索引。

输入格式

第一行包含一个整数 \(n\)\(1 \le n \le 10^6\)),表示树的顶点数量。

接下来有 \(n-1\) 行,每行包含两个整数 \(x\)\(y\)\(1 \le x, y \le n\)\(x \ne y\)),表示树中的一条边。

保证这些边构成一棵树。

输出格式

输出 \(n\) 个数字,第 \(i\) 个数字表示顶点 \(i\) 的主导索引

样例 #1
样例输入 #1
4
1 2
2 3
3 4
样例输出 #1
0
0
0
0
样例 #2
样例输入 #2
4
1 2
1 3
1 4
样例输出 #2
1
0
0
0
样例 #3
样例输入 #3
4
1 2
2 3
2 4
样例输出 #3
2
1
0
0

长链剖分模板题,但是用线段树合并。

这道题长链剖分没看出来,线段树合并一眼!

简单的说,我们要找每个点子树内最厚的深度(哪个深度有最多的点,这里的深度指以该点为根的深度),如果有多个答案,选择最浅的深度作为答案。

我们考虑对于每个点,将每个点的深度加入到该点的权值线段树上,然后将子树的线段树合并到该节点,最后全局查询最左的最大深度。

没什么多说的,上代码。

#include<bits/stdc++.h>
using namespace std;
		
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 1e6+5;
int n,dep[N],a[N];
int head[N],nxt[N<<1],to[N<<1],cnt;
void init() {memset(head,-1,sizeof(head));cnt = 0;}
void add(int u,int v) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	head[u] = cnt++;
}

int rt[N];
namespace sgt{
int mx[N * 25],ls[N * 25],rs[N * 25],tot;
#define mid ((pl + pr) >> 1)
void push_up(int p) {mx[p] = max(mx[ls[p]],mx[rs[p]]);}
void update(int &p,int pl,int pr,int k) {
	if(!p) p = ++tot;
	if(pl == pr) {mx[p]++;return;}
	if(k <= mid) update(ls[p],pl,mid,k);
	else update(rs[p],mid+1,pr,k);
	push_up(p);
}

int query(int p,int pl,int pr) {
	if(!p) return 0;
	if(pl == pr) return pl;
	if(mx[ls[p]] >= mx[rs[p]]) return query(ls[p],pl,mid);
	else return query(rs[p],mid + 1,pr);
}

int merge(int x,int y,int pl,int pr) {
	if(!x || !y) return x + y;
	if(pl == pr) {
		mx[x] += mx[y];
		return x;
	}
	ls[x] = merge(ls[x],ls[y],pl,mid);
	rs[x] = merge(rs[x],rs[y],mid+1,pr);
	push_up(x);
	return x;
}

}


void dfs(int x,int f) {
	dep[x] = dep[f] + 1;
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f) {
			dfs(y,x);
			rt[x] = sgt::merge(rt[x],rt[y],1,n);
		}
	}
	sgt::update(rt[x],1,n,dep[x]);
	a[x] = sgt::query(rt[x],1,n) - dep[x];
}

signed main() {
	init();
	n = rd();
	for(int i = 1;i<n;i++) {
		int u = rd(),v = rd();
		add(u,v);add(v,u);
	}
	dfs(1,0);
	for(int i = 1;i<=n;i++) wt(a[i]),putchar('\n');
	return 0;
}

2024/09/09#

P4197 Peaks#

题面:

题目描述

在 Bytemountains 有 \(n\) 座山峰,每座山峰有他的高度 \(h_i\)。有些山峰之间有双向道路相连,共 \(m\) 条路径,每条路径有一个困难值,这个值越大表示越难走。

现在有 \(q\) 组询问,每组询问询问从点 \(v\) 开始只经过困难值小于等于 \(x\) 的路径所能到达的山峰中第 \(k\) 高的山峰,如果无解输出 \(-1\)

输入格式

第一行三个数 \(n,m,q\)
第二行 \(n\) 个数,第 \(i\) 个数为 \(h_i\)

接下来 \(m\) 行,每行三个整数 \(a,b,c\),表示从 \(a \to b\) 有一条困难值为 \(c\) 的双向路径。
接下来 \(q\) 行,每行三个数 \(v,x,k\),表示一组询问。

输出格式

对于每组询问,输出一个整数表示能到达的山峰中第 \(k\) 高的山峰的高度。

样例 #1
样例输入 #1
10 11 4
1 2 3 4 5 6 7 8 9 10
1 4 4
2 5 3
9 8 2
7 8 10
7 1 4
6 7 1
6 4 8
2 1 5
10 8 10
3 4 7
3 4 6
1 5 2
1 5 6
1 5 8
8 9 2
样例输出 #1
6
1
-1
8
提示
数据规模与约定

对于 \(100\%\) 的数据,\(n \le 10^5\)\(0 \le m,q \le 5\times 10^5\)\(h_i,c,x \le 10^9\)

不难看出本题是 Kruskal 重构树

建出 Kruskal 最小树重构树后倍增找到可达的最远的 lca ,

然后,对于子树中的叶子进行第 \(k\) 大查询!

然而叶子的编号不是连续的,我们很难用什么数据结构维护。

考虑改变编号,我们用 dfs 序去遍历树,如图:

img

我们发现,因为 dfs 序的性质,一个节点的子树被访问完了之后,才会退出该节点,所以一个子树的叶子节点一定构成一个连续区间,

这样我们就将题目变成了区间第 \(k\) 大查询,用可持久化线段树就可以维护出来了.

因为可持久化线段树维护的值可以离散化,果断离散化减少空间

#include<bits/stdc++.h>
using namespace std;
#define int long long
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 1e5+5,M = N * 16,maxn = N * 16;
int n,m,q,h[maxn],s[maxn],k,v[maxn];
array<int,3> e[M];
int head[maxn],nxt[maxn<<1],to[maxn<<1],cnt;
void init() {memset(head,-1,sizeof(head));}
int find(int x) {
	if(s[x] ^ x) s[x] = find(s[x]);
	return s[x];
}
void add(int u,int V) {
	nxt[cnt] = head[u];
	to[cnt] = V;
	head[u] = cnt++;
}

void kruskal() {
	k = n;
	int tot = 0;
	for(int i = 0;i<maxn;i++) s[i] = i;
	sort(e + 1,e + m + 1);
	for(int i = 1;i<=m;i++) {
		int fx = find(e[i][1]),fy = find(e[i][2]);
		if(fx ^ fy) {
			s[fx] = s[fy] = ++k;
			add(fx,k);add(fy,k);
			add(k,fx);add(k,fy);
			v[k] = e[i][0];
			tot++;
			if(tot == n - 1) break;
		}
	}
}

int fa[maxn][21],hd[maxn][21],L[maxn],R[maxn],tot;
int rt[maxn],lst[maxn];
namespace sgt{
#define mid ((pl + pr) >> 1)
int rot,siz[N*50],ls[N*50],rs[N*50];
int update(int f,int pl,int pr,int x) {
	int rt = ++rot;
	ls[rt] = ls[f];
	rs[rt] = rs[f];
	siz[rt] = siz[f] + 1;
	if(pl < pr) {
		if(x <= mid) ls[rt] = update(ls[f],pl,mid,x);
		else rs[rt] = update(rs[f],mid+1,pr,x);
	}
	return rt;
}

int query(int x,int y,int pl,int pr,int K) {
	if(siz[y] - siz[x] < K) return -1;
	if(pl == pr) return pl;
	int sz = siz[rs[y]] - siz[rs[x]];
	if(K <= sz) return query(rs[x],rs[y],mid + 1,pr,K);
	else return query(ls[x],ls[y],pl,mid,K - sz);
}

}

void dfs(int x,int f) {
	fa[x][0] = f;hd[x][0] = v[fa[x][0]];
	for(int i = 1;i<=20;i++) {
		fa[x][i] = fa[fa[x][i - 1]][i - 1]; 
		hd[x][i] = min(hd[fa[x][i - 1]][i - 1],hd[x][i]);
	}
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f) {
			dfs(y,x);
			L[x] = min(L[x],L[y]);
			R[x] = max(R[x],R[y]);
		}
	}
	if(x <= n) {
		R[x] = L[x] = ++tot;
		rt[tot] = sgt::update(rt[tot - 1],1,N,h[x]);
	}
}


signed main() {
	init();
    n = rd(),m = rd(),q = rd();
	for(int i = 1;i<=n;i++) lst[i] = h[i] = rd();
	for(int i = 1;i<=m;i++) 
		e[i][1] = rd(),e[i][2] = rd(),e[i][0] = rd();
	sort(lst + 1,lst + n + 1);
	int top = unique(lst + 1,lst + n + 1) - lst - 1;
	for(int i = 1;i<=n;i++)
		h[i] = lower_bound(lst + 1,lst + top + 1,h[i]) - lst;
	kruskal();
	memset(L,0x3f,sizeof(L));
	memset(hd,0x3f,sizeof(hd));
	v[0] = 0x3f3f3f3f3f3f3f3fLL;
	dfs(k,0);
	while(q--) {
		int S = rd(),x = rd(),K = rd();
		for(int i = 20;i >= 0;i--) 
			if(hd[S][i] <= x) 
				S = fa[S][i];
		if(sgt::siz[rt[R[S]]] - sgt::siz[rt[L[S] - 1]] < K) {puts("-1");continue;}
		int ans = sgt::query(rt[L[S] - 1],rt[R[S]],1,N,K);
		if(ans == -1) puts("-1");
		else wt(lst[ans]),putchar('\n');
	}

	return 0;
}

2024/09/12#

CF888G Xor-MST#

题面:

题面翻译
  • 给定 \(n\) 个结点的无向完全图。每个点有一个点权为 \(a_i\)。连接 \(i\) 号结点和 \(j\) 号结点的边的边权为 \(a_i\oplus a_j\)
  • 求这个图的 MST 的权值。
  • \(1\le n\le 2\times 10^5\)\(0\le a_i< 2^{30}\)
样例 #1
样例输入 #1
5
1 2 3 4 5
样例输出 #1
8
样例 #2
样例输入 #2
4
1 2 3 4
样例输出 #2
8

这里的图是一个完全图,直接跑 \(Kruskal\)\(Prim\) 都会炸掉。

于是 \(Boruvka\) MST算法出现了。

简要说明一下这个算法的本质:

个人认为,这个算法类似于分治,从子问题出发,直到求解出全局答案。(一种 \(Kruskal\)\(Prim\) 的结合)

  1. 最初,每个点分别归属于一个集合,

  2. 然后,找出集合之间前 \(\lfloor \frac{n}{2} \rfloor\) 小边(这些边的端点不重合),连接,并合并两个区间

  3. 重复过程 \(2\) 直到只剩下一个集合

因为过程 \(2\) 会把 \(n\) 个集合缩小到 \(\lceil \frac{n}{2} \rceil\) 个集合,时间复杂度显然是 \(\mathcal{O}(n \log n)\)

但是考题中一般不裸考 \(Boruvka\) MST,而关键在于 \(Boruvka\) 算法的思想,自下而上的合并手段

回到本题,看到异或自然想到 建一颗 \(01\) Trie,

关键在于,在 \(01\) Trie 上反向 \(Borvurka\) !

当我们在判断 Trie 上某一位时,如果左右儿子都存在(即 下一位是 \(0、1\) 的数都出现过)

因为每个点都要相连,而连接时有异或最小的限制,

所以在此时,我们假定 左子树中所有数已经在一个集合里,右子树同理,

然后如同 \(Borvurka\) 的运行过程一样,现在我们要将左右两个集合以最小代价连接起来。

在右子树中找到左子树中每个数在右子树中数的异或最小值,再加上该位左右子树合并的代价 \((即 (1\ <<\ dep))\)

然后递归去做左、右子树中分别建出一颗树的最小代价。

有人说像 反向 \(Kruskal\) 重构树,这样说也不算错,如果有助于理解,也可以这样思考.

在具体实现上,在做 在右子树中找到左子树中每个数在右子树中数的异或最小值时,

将最初的数排序插入,这样左子树和右子树上的数的编号(下标)连续。

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long 
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 2e5+5;
int n,a[N],ls[N * 40],rs[N * 40],rt,tot,L[N * 40],R[N * 40];
void insert(int &p,int x,int dep) {
	if(!p) p = ++tot;
	L[p] = min(L[p],x);
	R[p] = max(R[p],x);
	if(dep < 0) return;
	((a[x] >> dep) & 1) ? insert(rs[p],x,dep - 1) : insert(ls[p],x,dep - 1);
}
int query(int p,int v,int dep) {
	if(dep < 0) return 0;
	int k = (v >> dep) & 1;
	if(k) {
		if(rs[p]) return query(rs[p],v,dep - 1);
		else return query(ls[p],v,dep - 1) + (1 << dep);
	}else {
		if(ls[p]) return query(ls[p],v,dep - 1);
		else return query(rs[p],v,dep - 1) + (1 << dep);
	}
}

long long dfs(int p,int dep) {
	if(dep < 0) return 0;
	if(R[ls[p]] && R[rs[p]]) {
		int mi = 0x3f3f3f3f;
		for(int i = L[ls[p]];i<=R[ls[p]];i++) mi = min(mi,query(rs[p],a[i],dep - 1));
		return dfs(ls[p],dep - 1) + dfs(rs[p],dep - 1) + mi + (1 << dep);
	}
	if(R[ls[p]]) return dfs(ls[p],dep - 1);
	else if(R[rs[p]]) return dfs(rs[p],dep - 1);
	return 0;
}

signed main() {
	n = rd();
	for(int i = 1;i<=n;i++) 
		a[i] = rd();
	sort(a + 1,a + n + 1);
	memset(L,0x3f,sizeof(L));
	for(int i = 1;i<=n;i++) insert(rt,i,30);
	wt(dfs(rt,30));
	return 0;
}

Gym-101194G Pandaria#

简要题意:

给你一个 \(n(n ≤ 10^5)\) 个点,\(m(m ≤ 2 × 10^5)\) 条边的无向图,

每个点有一个颜色 \(c_i\),每条边有一个边权 \(w_i\)
\(q(q ≤ 2 × 10^5)\) 组询问 \((x, w)\),每次询问从点 \(x\) 出发,只经过边权不超过 \(w\) 的边所能到达的连通块中,出现次数最多的颜色中,编号最小的颜色是多少?
多测且强制在线

遇到限制 \(\Rightarrow\) \(Kruskal\) 重构树 + 倍增,

子树颜色最大值查询 \(id\) \(\Rightarrow\) 树上线段树合并

强制在线 \(\Rightarrow\) 离线查询每个 \(Kruskal\) 重构树上的点的答案

然后就没有然后了,很水,但是确实比较板子

AC-code:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
ll rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(ll x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = (1e5+5),M = 2e5+5,maxN = N << 2;
int n,m,q,s[maxN],T,co[N],tot,V[maxN];
int fa[maxN][21];
ll w[maxN][21];
array<int,3> e[M];
int head[maxN],nxt[maxN<<1],to[maxN<<1],cnt;

void init() {
	for(int i = 0;i<maxN;i++) s[i] = i;
	memset(head,-1,sizeof(head));
	memset(V,0x3f,sizeof(V));
	memset(w,0x3f,sizeof(w));
	memset(fa,0,sizeof(fa));
	cnt = 0;tot = n;
}

void add(int u,int v) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	head[u] = cnt++;
}

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

void kruskal() {
	tot = n;
	sort(e + 1,e + m + 1);
	for(int i = 1;i<=m;i++) {
		int fx = find(e[i][1]),fy = find(e[i][2]);
		if(fx == fy) continue;
		s[fx] = s[fy] = ++tot;
		add(fx,tot);add(fy,tot);
		add(tot,fx);add(tot,fy);
		V[tot] = e[i][0];
		if(tot - n == n - 1) break;
	}
}

int rt[maxN];
namespace sgt{
#define mid ((pl + pr) >> 1)
int ls[maxN * 30],rs[maxN * 30],siz[maxN * 30],rot;
void init() {
	rot = 0;
	memset(ls,0,sizeof(ls));
	memset(rs,0,sizeof(rs));
	memset(siz,0,sizeof(siz));
	memset(rt,0,sizeof(rt));
}
void push_up(int p) {siz[p] = max(siz[ls[p]],siz[rs[p]]);}
void update(int &p,int pl,int pr,int v) {
	if(!p) p = ++rot;
	if(pl == pr) {siz[p]++;return;};
	if(v <= mid) update(ls[p],pl,mid,v);
	else update(rs[p],mid+1,pr,v);
	push_up(p);
}

int query(int p,int pl,int pr) {
	if(pl == pr) return pl;
	if(siz[ls[p]] >= siz[rs[p]]) return query(ls[p],pl,mid);
	else return query(rs[p],mid+1,pr);
}

int merge(int x,int y,int pl,int pr) {
	if(!x || !y) return x + y;
	if(pl == pr) {
		siz[x] += siz[y];
		return x;
	} 
	ls[x] = merge(ls[x],ls[y],pl,mid);
	rs[x] = merge(rs[x],rs[y],mid+1,pr);
	push_up(x);
	return x;
}

}
#undef mid

int ans[maxN];

void dfs(int x,int f) {
	if(x <= n) sgt::update(rt[x],1,N,co[x]);
	fa[x][0] = f;w[x][0] = V[f];
	for(int i = 1;i<=20;i++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
	for(int i = 1;i<=20;i++) w[x][i] = w[fa[x][i - 1]][i - 1];
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f) {
			dfs(y,x);
			rt[x] = sgt::merge(rt[x],rt[y],1,N);
		}
	}
	ans[x] = sgt::query(rt[x],1,N);
}

int X,Y,last = 0,popAndPipi;

void solve() {
	n = rd(),m = rd();
	for(int i = 1;i<=n;i++) co[i] = rd();
	init();sgt::init();
	for(int i = 1;i<=m;i++)
		e[i][1] = rd(),e[i][2] = rd(),e[i][0] = rd();
	kruskal();
	dfs(tot,0);
	putchar('C'),putchar('a'),putchar('s'),putchar('e'),putchar(' '),putchar('#'),wt(++popAndPipi),putchar(':'),putchar('\n');
	q = rd();
	last = 0;
	while(q--) {
		X = rd() ^ last,Y = rd() ^ last;
		for(int i = 20;i >= 0;i--)
			if(w[X][i] <= Y) X = fa[X][i];
		wt(last = ans[X]);
		putchar('\n');
	}
}

signed main() {
	T = rd();
	while(T--) solve();	
	return 0;
}

2024/09/14#

CF843D Dynamic Shortest Path#

题面:

题面翻译

有一张 \(n\) 个点 \(m\) 条边的有向带权图,你需要回答如下的 \(q\) 个问题:

  1. 1 v 询问以 \(1\) 为起点到 \(v\) 的最短路
  2. 2 c l_1 l_2 ... l_c 对于 \(l_1, l_2, \ldots, l_c\) 的边的边权增加 \(1\)

不存在输出 -1

\(1 \leq n, m \leq 10^5\)\(1 \leq q \leq 2000\)

样例 #1
样例输入 #1
3 2 9
1 2 0
2 3 0
2 1 2
1 3
1 2
2 1 1
1 3
1 2
2 2 1 2
1 3
1 2
样例输出 #1
1
0
2
1
4
2
样例 #2
样例输入 #2
5 4 9
2 3 1
2 4 1
3 4 1
1 2 0
1 5
1 4
2 1 2
2 1 2
1 4
2 2 1 3
1 4
2 1 4
1 4
样例输出 #2
-1
1
2
3
4
提示

The description of changes of the graph in the first sample case:

The description of changes of the graph in the second sample case:

很有趣,但很难卡常(可能是我用了array<> 和 queue<>

我们每次跑一遍最短路肯定行不通,那么只有一种方法,不用每次 迪杰斯特拉 和 Spfa \(\rightarrow\) BFS,将复杂度 \(O(n \log n) \rightarrow O(n)\)

但普通的 BFS 显然不能解决问题,它只能处理边权为 \(1\) 的图

观察数据规模,发现 \(q\) 很小,从 \(q\) 入手

\(q\) 的规模代表一个边的最大 \(\Delta\) 值,那么一条最短路的增量 \(\Delta\) 其实很有限。

因为一共 \(10^5\) 个 点,最短路最多只有 \(10^5 - 1\) 个边,故 \(\Delta_{\max} = 10^5 - 1 + q\)

读者可以去学习 01-bfs ,我们在这题要用到的思路是 \(桶 + \text{BFS}\)

我们考虑给增量单独建图,先跑一个最短路,记录到 \(dis\)

新图边 \((u,v)\) 的边权就是 \(dis_v - dis_u + w\)

对于每一次 \(+1\) 我们把对应边的 \(w + 1\)

然后给增量图跑最短路就可以得到新图的最短路增量,将其叠加上原图即是答案。

如何给增量图跑一个 \(O(n)\) 的最短路呢?

我们可以给每个最短路值开一个队列,

因为最短路最大有用增量边权 \(= \Delta_{\max}\),所以开 \(\Delta_{\max}\) 个队列,

因为跑最短路时,每个点的最短路单调更新,当一个最短路值的队列全部更新完了后,这个队列不会有新的点加入

直接指针挪向后一个权值的队列,继续更新,直到指针超过了 \(\Delta_{\max}\),这样就可以轻松实现 \(O ( n )\) 最短路

然后每次操作并将答案覆盖原最短路值。

为什么是正确的呢?

如果没有操作,

一个不在 \(s \rightsquigarrow t\) 边,则 \(w^{\prime} = dis_v - dis_u + w\) ,这时 \(w^{\prime} \not= 0\)

而在最短路上的边 \(w^{\prime} = dis_v - dis_u + w = 0\) 优先走,答案显然正确。

如果有操作,那么我们就有可能走之前可能不优但现在变优的边,这时增量自动就往小的方向走了,答案显然正确。


本人用STL中的 queue 和 array 所以需要卡复杂度。

如果没有使用 STL 但还是没过的人,可以考虑将头文件拆开,只使用必要的头文件。

如果你 \(bfs\)\(dij\)\(q.size()\) 来判断是否进行循环,那么这个时间会大不少!

一定要用 \(q.empty()\) 判断队列非空

如果你像我一样唐,可以考虑用 SPFA 加 SLF、LLL 优化 代替 迪杰斯特拉,在带上一个超级快读

AC-code:

#include <cstdio>
#include <queue>
using namespace std;
using LL = long long;
#define int long long
constexpr int N = 1e5+5,inf = 0x3f3f3f3f3f3f3f3fLL;
char *p1, *p2, buf[N];
#define nc() (p1 == p2 && (p2 = (p1 = buf) +\
fread(buf, 1, N, stdin), p1 == p2) ? EOF : *p1 ++ )
LL rd()
{
    LL x = 0, f = 1;
    char ch = nc();
    while (ch < 48 || ch > 57)
    {
        if (ch == '-') f = -1;
        ch = nc();
    }
    while (ch >= 48 && ch <= 57)
        x = (x << 3) + (x << 1) + (ch ^ 48), ch = nc();
    return x * f;
}

char obuf[N], *p3 = obuf;
#define putchar(x) (p3 - obuf < N) ? (*p3 ++ = x) :\
(fwrite(obuf, p3 - obuf, 1, stdout), p3 = obuf, *p3 ++ = x)
inline void wt(LL x)
{
    if (!x)
    {
        putchar('0');
        return;
    }
    LL len = 0, k1 = x, c[40];
    if (k1 < 0) k1 = -k1, putchar('-');
    while (k1) c[len ++ ] = k1 % 10 ^ 48, k1 /= 10;
    while (len -- ) putchar(c[len]);
}

int _min(int x,int y) {return x > y ? y : x;}
int _max(int x,int y) {return x > y ? x : y;}

int d[N],f[N],n,m,Q;
int head[N],nxt[N<<1],to[N<<1],val[N<<1],cnt;
int vis[N],inq[N];
void init() {for(int i = 1;i<=n;i++) head[i] = -1;} 
void add(int u,int v,int w) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	val[cnt] = w;
	head[u] = cnt++;
} 

void spfa() {
	for(int i = 1;i<=n;i++) d[i] = inf;
	d[1] = 0;
	deque<int> q;
	q.push_front(1);
	inq[1] = true;
	int sum = d[1];
	int num = 1;
	while(!q.empty()) {
		int t = q.front();
		while(d[t]*num>sum){
			q.pop_front();
			q.emplace_back(t);
			t=q.front();
		}
		q.pop_front();
		inq[t] = false;
		sum -= d[t];
		num--;
		for(int i = head[t];~i;i = nxt[i]) {
			int y = to[i];
			if(d[y] > d[t] + val[i]) {
				d[y] = d[t] + val[i];
				if(!inq[y]) {
					inq[y] = true;
					if(!q.empty() && d[q.front()] < d[y]) q.emplace_back(y);
					else q.emplace_front(y);
					sum += d[y];
					num++;
				}
			}
		}	
	}
}

queue<int> t[N];
int c,maxx;
void bfs() {
	for(int i = 1;i<=n;i++) f[i] = inf;
	f[1] = 0;
	t[0].emplace(1);
	maxx = 0;
	for(int i = 0;i<=maxx;i++) {
		while(!t[i].empty()) {
			int x = t[i].front();
			t[i].pop();
			if(f[x] < i) continue;
			for(int i = head[x];~i;i = nxt[i]) {
				int y = to[i],w = val[i];
				int z = d[x] - d[y] + w;
				if(f[y] > f[x] + z) {
					f[y] = f[x] + z;
					if(f[y] <= n - 1){
						t[f[y]].emplace(y);
						maxx = _max(maxx,f[y]);
					}
				}
			}
		}
	}
}

void query() {
	int v = rd();
	if(d[v] == inf) wt(-1),putchar('\n');
	else wt(d[v]),putchar('\n');
}

void update() {
	c = rd();
	for(int i = 1;i<=c;i++) {
		int x = rd();
		val[x - 1]++;
	}
	bfs();
	for(int i = 1;i<=n;i++) d[i] = _min(inf,d[i] + f[i]);
}

signed main() {
	n = rd(),m = rd(),Q = rd();
	init();
	for(int i = 1;i<=m;i++) {
		int u = rd(),v = rd(),w = rd();
		add(u,v,w);
	}
	spfa();
	while(Q--) {
		int opt = rd();
		switch(opt) {
			case 1:
				query();
				break;
			case 2:
				update();
				break;
		}
	}
    fwrite(obuf, p3 - obuf, 1, stdout);
	return 0;
}

附上我通过时的图,时间限制为 \(10s\)

img

2024/09/15#

CF416E President's Path#

题面:

题面翻译

对于 \(n\) 个点 \(m\) 条边的简单无向图(无重边、自环),记 \(f(s,t)\)\(R_{s,t}\) 类边的数量。\(R_{s,t}\) 类边是指这样一类边:存在一条从 \(s\)\(t\) 的最短路,满足这条边在最短路上。

对于每一对 \(s,t\) ,你都要计算出 \(f(s,t)\) 的值。

输入格式

先是 \(n,m(2\le n\le 500,0\le m\le \frac{n(n-1)}{2})\) ,含义见题面。

接下来 \(m\) 行表述边 \((x_i,y_i,l_i)\) ,分别为连接的两个点的编号,边的长度。

输出格式

仅输出一行,包含 \(\frac{n(n-1)}{2}\) 个数。前 \(n-1\) 个数为 \(f(1,2),f(1,3),\dots,f(1,n)\) ;接下来 \(n-2\) 个数为 \(f(2,3),f(2,4),\dots,f(2,n)\) ;以此类推。

样例 #1
样例输入 #1
5 6
1 2 1
2 3 1
3 4 1
4 1 1
2 4 2
4 5 4
样例输出 #1
1 4 1 2 1 5 6 1 2 1

观察 \(n\) 的数据范围,自然想到跑 \(floyd\).

我们如果枚举每一条边,判断是否在最短路上,

对于最短路边 \((u,v)\)

\[dis_{i,j} = dis_{i,u} + dis_{v,j} + w(u,v) \]

时间复杂度是 \(\mathcal{O}(n^4)\) 不能接受.

考虑优化到 \(\mathcal{O}(n^3)\),

枚举点是否在最短路上,

对于最短路点 \(k\)

\[ dis_{i,j} = dis_{i,k} + dis_{k,j} \]

\(dp_{i,j}\)\(i \rightsquigarrow j\) 上最短边的个数,

根据最短路(一条链)的性质,对于 \(i \rightsquigarrow k \rightsquigarrow j\),我们考虑以计算 \((x,k)\) 边的个数,累加到 \(dp_{i,j}\)

我们如何计算 \((x,k)\) 呢?

我们将边备份给 \(g_{i,j}\),然后对于 \(i\rightsquigarrow k\) 计算 \(i \rightsquigarrow x \rightarrow k\) 的个数,用:

\[dis_{i,k} = dis_{i,x} + g_{x,k} \]

判断。

然后枚举起点 \(i\),以上述方法更新 \(dp_{i,j}\)

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
constexpr int N = 505,inf = 0x3f3f3f3f3f3f3f3fLL;
int n,m,f[N][N],g[N][N],dp[N][N],con[N];

signed main() {
	n = rd(),m = rd();
	for(int i = 1;i<=n;i++)
		for(int j = 1;j<=n;j++)
			f[i][j] = f[j][i] = g[i][j] = g[j][i] = inf;
	for(int i = 1;i<=m;i++) {
		int u = rd(),v = rd(),w = rd();
		g[u][v] = g[v][u] = f[u][v] = f[v][u] = min(w,f[u][v]);
	}
	for(int i = 1;i<=n;i++) f[i][i] = 0;
	for(int k = 1;k<=n;k++)
		for(int i = 1;i<=n;i++)
			for(int j = 1;j<=n;j++)
				f[j][i] = f[i][j] = min(f[i][j],f[i][k] + f[k][j]);
	for(int i = 1;i<=n;i++)  {
		for(int j = 1;j<=n;j++) con[j] = 0;
		for(int j = 1;j<=n;j++)
			for(int k = 1;k<=n;k++)
				if(f[i][k] + g[k][j] == f[i][j]) 
					con[j]++;
		for(int j = 1;j<=n;j++)
			for(int k = 1;k<=n;k++)
				if(f[i][k] + f[k][j] == f[i][j])
					dp[i][j] += con[k];
	}
	for(int i = 1;i<=n;i++)
		for(int j = i + 1;j<=n;j++) {
			if(f[i][j] >= inf) dp[i][j] = 0;
			wt(dp[i][j]),putchar(' ');
		}
	
					
	return 0;
}

CF986F Oppa Funcan Style Remastered#

题面:

题面翻译

给定 \(n\)\(k\),问是否能将 \(n\) 分为若干个 \(k\) 的因数(\(1\) 除外)之和,每个因数都可以被选多次。

\(n\leq 10^{18}\)\(k\leq 10^{15}\),最多 \(50\) 种不同的 \(k\)

一共 \(t\) 组询问,\(t\leq 10^4\)

样例 #1
样例输入 #1
3
7 7
3 8
5 6
样例输出 #1
YES
NO
YES

\(k\) 只有 \(50\) 种,考虑离线一起求解

分解 \(k\),因为 \(k \leq 10^{15}\),所以预处理质因数 \(\sqrt{10^{15}} \approx 3.2 \times 10^7\)

分解质因数,分类讨论(因为质因数如果只有 \(1\) 个的话可以直接判断;只有 \(2\) 个的话,因为分解出来很大,不适合跑最短路,可以用同余式直接求解)

  1. 如果质因数有 \(1\)
    判断 \([k \mid x]\) 即可

  2. 如果质因数有 \(2\) 个,设为 \((a,b)\)
    那么:

\[\begin{align*} xa + yb = n &\Rightarrow ax \equiv n &(\operatorname{mod}\ b) \\ &\Rightarrow x \equiv \frac{n}{a} &(\operatorname{mod}\ b) \end{align*} \]

只要找到最小的 \(ax\),和 \(n\) 判断一下就可以了

  1. 如果质因数有 \(3\) 个以上,那么,我们相当于在求,对于 质因数\((a,b,c,\ldots)\)

\[ ax + by + cz \ldots = n \]

是否有 \((x,y,z,\ldots)\) 的可行解。

我们自然想到同余最短路,判断在 \(\operatorname{mod}\ a\) 下与 \(n\) 同余的最小值 \(x\),是否比 \(n\) 大。

这样这题就解决了!

AC-code:

#include<bits/stdc++.h>
using namespace std;	
long long rd() {
	long long x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}
const int N = 1e5+5,M = 3.2e7;
int p[M],top;
bool pr[M];
void init() {
	pr[1] = 1;
	for(int i = 2;i < M;i++) {
		if(!pr[i]) p[++top] = i;
		for(int j = 1;j<=top && i * p[j] < M;j++) {
			pr[(long long)i * p[j]] = true;
			if(i % p[j] == 0) break;
		}
	}
}

long long a[N];

void dec(long long x,int &t) {
	t = 0;
	for(int i = 1;i<=top && p[i] * p[i] <= x;i++) {
		if(x % p[i] == 0) {
			a[++t] = p[i];
			while(x % p[i] == 0) x /= p[i];
		}
	}
	if(x > 1) a[++t] = x;
}

long long qpow(long long X,int K,int mod) {
	long long re = 1;
	X %= mod;
	while(K) {
		if(K & 1) (re *= X) %= mod;
		(X *= X) %= mod;
		K >>= 1;
	}
	return re;
}

long long dis[N],vis[N];
bool ans[N];
map<long long,vector<array<long long,2>>> mp;

void solve(long long k,vector<array<long long,2>> &id) {
	int tp = 0;
	if(k == 1) return;
	dec(k,tp);
	if(tp == 1) {
		for(array<long long,2> t : id) 
			ans[t[1]] = t[0] % a[1] ? 0 : 1;
	}
	else if(tp == 2) {
		for(array<long long,2> t : id) {
			long long K = t[0] % a[1] * qpow(a[2],a[1] - 2,a[1]) % a[1];
			ans[t[1]] = K * a[2] <= t[0];
		}
	}
	else {
		memset(dis,0x3f,sizeof(dis));
		for(int i = 0;i<=a[1];i++) vis[i] = 0;
		dis[0] = 0;
		priority_queue<array<long long,2>> q;
		q.emplace(array<long long,2>{0,0});
		while(!q.empty()) {
			long long t = q.top()[1];
			q.pop();
			if(vis[t]) continue;
			vis[t] = true;
			for(int i = 2;i<=tp;i++) {
				long long y = (long long)((long long)t + (long long)a[i]) % a[1];
				if(dis[y] > dis[t] + a[i]) {
					dis[y] = dis[t] + a[i];
					q.emplace(array<long long,2>{-dis[y],y}); 
				}
			}
		}
		for(auto t : id)
			ans[t[1]] = dis[t[0] % a[1]] <= t[0];
	}
	return;
}

signed main() {
	init();
	int t = rd();
	for(int i = 1;i<=t;i++) {
		long long x = rd(),y = rd();
		mp[y].emplace_back(array<long long,2>{x,i});
	}
	for(auto tp : mp) solve(tp.first,tp.second);
	for(int i = 1;i<=t;i++)
		ans[i] ? puts("YES") : puts("NO");
	return 0;
}

2024/09/16#

P3812 【模板】线性基#

题面:

题目描述

给定 \(n\) 个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。

输入格式

第一行一个数 \(n\),表示元素个数

接下来一行 \(n\) 个数

输出格式

仅一行,表示答案。

样例 #1
样例输入 #1
2
1 1
样例输出 #1
1
样例 #2
样例输入 #2
4
1 5 9 4
样例输出 #2
13
提示

$ 1 \leq n \leq 50, 0 \leq S_i < 2 ^ {50} $

异或线性基;

建议观看董晓算法的视频,讲的非常好,记得三联

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long		
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 55;
int p[63],n;

void greedy(int x) {
	for(int i = 50;i>=0;i--) {
		if(x >> i & 1) {
			if(!p[i]) {
				p[i] = x;
				break;
			}
			x ^= p[i];
		}
	}
}

signed main() {
	n = rd();
	for(int i = 1;i<=n;i++) {
		int x = rd();
		greedy(x);
	}
	int ans = 0;
	for(int i = 50;i >= 0;i--)
		ans = max(ans,ans ^ p[i]);
	wt(ans);
	return 0;
}

P4570 [BJWC2011] 元素#

题面:

题目描述

相传,在远古时期,位于西方大陆的 Magic Land 上,人们已经掌握了用魔法矿石炼制法杖的技术。那时人们就认识到,一个法杖的法力取决于使用的矿石。

一般地,矿石越多则法力越强,但物极必反:有时,人们为了获取更强的法力而使用了很多矿石,却在炼制过程中发现魔法矿石全部消失了,从而无法炼制出法杖,这个现象被称为“魔法抵消” 。特别地,如果在炼制过程中使用超过一块同一种矿石,那么一定会发生“魔法抵消”。后来,随着人们认知水平的提高,这个现象得到了很好的解释。经过了大量的实验后,著名法师 Dmitri 发现:如果给现在发现的每一种矿石进行合理的编号(编号为正整数,称为该矿石的元素序号),那么,一个矿石组合会产生“魔法抵消”当且仅当存在一个非空子集,那些矿石的元素序号按位异或起来为零(如果你不清楚什么是异或,请参见下一页的名词解释 )。

例如,使用两个同样的矿石必将发生“魔法抵消”,因为这两种矿石的元素序号相同,异或起来为零。并且人们有了测定魔力的有效途径,已经知道了:合成出来的法杖的魔力等于每一种矿石的法力之和。人们已经测定了现今发现的所有矿石的法力值,并且通过实验推算出每一种矿石的元素序号。

现在,给定你以上的矿石信息,请你来计算一下当时可以炼制出的法杖最多有多大的魔力。

输入格式

第一行包含一个正整数 \(N\),表示矿石的种类数。

接下来 \(N\) 行,每行两个正整数\(\mathrm{Number}_i\)\(\mathrm{Magic}_i\),表示这种矿石的元素序号和魔力值。

输出格式

仅包含一行,一个整数代表最大的魔力值。

样例 #1
样例输入 #1
3 
1 10 
2 20 
3 30
样例输出 #1
50
提示
样例解释

由于有“魔法抵消”这一事实,每一种矿石最多使用一块。

如果使用全部三种矿石,由于三者的元素序号异或起来:\(1\ \mathrm{xor}\ 2\ \mathrm{xor}\ 3 = 0\) ,则会发生魔法抵消,得不到法杖。

可以发现,最佳方案是选择后两种矿石,法力为 \(20+30=50\)

数据范围

对于全部的数据:\(1\leq N \leq 1000\)\(1\leq \mathrm{Number}_i \le 10^{18}\)\(1\leq \mathrm{Magic}_i \le 10^4\)

我们知道贪心法构造的线性基,可以保证线性基里的元素可以异或且异或和最大

这道题可以先从大到小排序,插入进线性基。

为什么最优?我们将每一个数位看作一个坑;每可以插入一个数,相当于填了一个数位的坑。

对于每个数,它的数位决定了它能填哪个坑,如果它能填的坑都已经用价值大的填上了,这个数自然就填不上。

最后将所有数插入线性基,对于每个数位,将数位上填入的价值累加,就是答案。

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long	
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 2e3+5;
int n;
array<int,2> p[70],c[N];
void insert(array<int,2> k) {
	for(int i = 60;i >= 0;i--) {
		if(k[1] >> i & 1) {
			if(p[i][1]) 
				k[1] ^= p[i][1];
			else {
				p[i] = k;
				break;
			}
		}
	}
}
signed main() {
	n = rd();
	for(int i = 1;i<=n;i++)
		c[i][1] = rd(),c[i][0] = rd();
	for(int i = 0;i<70;i++) p[i][0] = p[i][1] = 0;
	sort(c + 1,c + n + 1,[&](array<int,2> a,array<int,2> b){return a[0] > b[0];});
	for(int i = 1;i<=n;i++)
		insert(c[i]);
	int ans = 0;
	for(int i = 0;i<=60;i++) ans += p[i][0];
	wt(ans);
	return 0;
}

CF1100F Ivan and Burgers#

题面:

题面翻译

给一个长为 \(n\) 的序列,\(q\) 次询问,问从 \(a_l,a_{l+1},\cdots,a_r\) 中选若干个数,异或和最大为多少。

\(1\le n,q\le 5\times 10^5\)\(0\le c_i\le 10^6\)

样例 #1
样例输入 #1
4
7 2 3 4
3
1 4
2 3
1 3
样例输出 #1
7
3
7
样例 #2
样例输入 #2
5
12 14 23 13 7
15
1 1
1 2
1 3
1 4
1 5
2 2
2 3
2 4
2 5
3 3
3 4
3 5
4 4
4 5
5 5
样例输出 #2
12
14
27
27
31
14
25
26
30
23
26
29
13
13
7

区间查询最大异或和(前缀和线性基)

类似于 可持久化线段树,读者可以类比

我们需要记录每个数位被数占领的时间和数值,

我们插入一个数,就建立一个新的版本,其的内容是复制上一个版本(这里是完全赋值,不是可持久化线段树的直接接上),

然后我们考虑插入数,如果有数位可以填入,就填入;如果有的数位填入的时间太早,我们考虑用这个数替换,然后用被替换的数继续做线性基

为什么要替换呢?因为我们记录每个数占领的时间,当我们询问 \((l,r)\) 时,对于版本 \(r\),有效的数需要满足 \(time_{i} \geq l\)

而这个位置不考虑插入时间时,可能有多个数字可以胜任,但是考虑插入时间后,其中能胜任的只有 \(time_{i} \geq l\),其中最保险的自然是 \(time_{i\max}\)

如果 \(time_{i\max}\) 都无法胜任,那么 插入时间为 \(r\) 之前的没有数字可以胜任,这样构造的答案一定合法。

大体思路就是这样,结合代码再思考应该就能理解。

AC-code:

#include<bits/stdc++.h>
using namespace std;
		
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 5e5 + 5;
int p[N][31],pos[N][31],n,m;

void insert(int x,int id) {
	for(int i = 0;i<=30;i++) {
		p[id][i] = p[id - 1][i];
		pos[id][i] = pos[id - 1][i];
	}
	int P = id;
	for(int i = 30;i >= 0;i--) {
		if(x >> i & 1) {
			// if(!p[id][i])  {
			// 	p[id][i] = x;
			// 	pos[id][i] = P;
			// 	break;
			// }
			if(pos[id][i] < P) 
				swap(p[id][i],x),swap(pos[id][i],P);
			x ^= p[id][i];
		}
	}
}

int query(int l,int r) {
	int ans = 0;
	for(int i = 30;i >= 0;i--) {
		if(pos[r][i] >= l) 
			ans = max(ans,ans ^ p[r][i]);
	}
	return ans;
}

signed main() {
	n = rd();
	for(int i = 1;i<=n;i++) {
		int x = rd();
		insert(x,i);
	}
	m = rd();
	while(m--) {
		int l = rd(),r = rd();
		wt(query(l,r));putchar('\n');
	}

	return 0;
}

2024/09/28#

[NOIP2018 提高组] 旅行#

题面:

题目背景

NOIP2018 提高组 D2T1

题目描述

小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。

小 Y 了解到,X 国的 \(n\) 个城市之间有 \(m\) 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。

小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可 以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到>上一个城市。当小 Y 回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。

为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 \(n\) 的序列。她希望这个序列的字典序 最小,你能帮帮她吗? 对于两个长度均为 \(n\) 的序列 \(A\)\(B\),当且仅当存在一个正整数 \(x\),满足以下条件时, 我们说序列 \(A\) 的字典序小于 \(B\)

  • 对于任意正整数 \(1 ≤ i < x\),序列 \(A\) 的第 \(i\) 个元素 \(A_i\) 和序列 \(B\) 的第 \(i\) 个元素 \(B_i\) 相同。
  • 序列 \(A\) 的第 \(x\) 个元素的值小于序列 \(B\) 的第 \(x\) 个元素的值。
输入格式

输入文件共 \(m + 1\) 行。第一行包含两个整数 \(n,m(m ≤ n)\),中间用一个空格分隔。

接下来 m 行,每行包含两个整数 \(u,v (1 ≤ u,v ≤ n)\) ,表示编号为 \(u\)\(v\) 的城市之 间有一条道路,两个整数之间用一个空格分隔。

输出格式

输出文件包含一行,\(n\) 个整数,表示字典序最小的序列。相邻两个整数之间用一个 空格分隔。

样例 #1
样例输入 #1
6 5 
1 3 
2 3 
2 5 
3 4 
4 6
样例输出 #1
1 3 2 5 4 6
样例 #2
样例输入 #2
6 6 
1 3 
2 3 
2 5 
3 4 
4 5 
4 6
样例输出 #2
1 3 2 4 5 6
提示

【数据规模与约定】

对于 \(100\%\) 的数据和所有样例, $1 \le n \le 5000 $ 且 \(m = n − 1\)\(m = n\)

对于不同的测试点, 我们约定数据的规模如下:

还是那个奇怪的数据范围: \(m = n - 1\) / \(m = n\)

这说明形成的图不是树,就是基环树。

先来考虑树:

如何得到字典序最小的解呢?

不难想到给边排序,让 dfs 每次往最小的节点走,记录 dfs序。

我们不需要在 dfs 的时候考虑这件事,

我们可以在建图的时候通过排序,直接确定走的顺序。

然后是基环树:

和普通的树的唯一区别是:有个环!

我们看到数据范围 \(n \leq 5000\) ,直接暴力断边,可以通过。

AC-code:

#include<bits/stdc++.h>
using namespace std;
#define int long long 
int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}
const int N = 5e5+5;
int n,m,s[N];
int head[N],nxt[N<<1],to[N<<1],cnt;
void init() {memset(head,-1,sizeof(head));cnt = 0;}
void add(int u,int v) {
	nxt[cnt] = head[u];
	to[cnt] = v;
	head[u] = cnt++;
}
array<int,2> e[N << 1];
vector<int> ans;
void dfs(int x,int fa) {
	ans.emplace_back(x);
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ fa) 
			dfs(y,x);
	}
}
int dep[N],fa[N],vis[N],son[N],siz[N],top[N],num,id[N];
void dfs1(int x,int f) {
	dep[x] = dep[f] + 1;
	fa[x] = f;
	siz[x] = 1;
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ f)  {
			dfs1(y,x);
			siz[x] += siz[y];
			if(siz[son[x]] < siz[y]) son[x] = y;
		}
	}
}

void dfs2(int x,int topx) {
	top[x] = topx;
	id[x] = ++num;
	if(!son[x]) return;
	dfs2(son[x],topx);
	for(int i = head[x];~i;i = nxt[i]) {
		int y = to[i];
		if(y ^ fa[x] && y ^ son[x]) dfs2(y,y);
	}
}

int cirmax;
void addvis(int u,int v) {
	while(u ^ v) {
		if(dep[u] < dep[v]) swap(u,v);
		vis[u] = true;
		cirmax = max(cirmax,u);
		u = fa[u];
	}
	vis[u] = true;
}

int u,v;

signed main() {
	// freopen("P5022_20.in","r",stdin);
	init();
	n = rd(),m = rd();
	for(int i = 1;i<=m;i++) 
		e[i][0] = rd(),e[i][1] = rd();
	for(int i = 1;i<=m;i++) 
		e[i + m][0] = e[i][1],e[i + m][1] = e[i][0];
	if(m == n - 1) {
		sort(e + 1,e + m + m + 1,[&](array<int,2> x,array<int,2> y) {
			if(x[0] == y[0]) return x[1] > y[1];
			return x[0] < y[0];
		});
		for(int i = 1;i<=m + m;i++) add(e[i][0],e[i][1]);
		dfs(1,0);
	}
	else {
		auto find = [&](auto self,int x) -> int{
			if(s[x] ^ x) s[x] = self(self,s[x]);
			return s[x];
		};
		for(int i = 1;i<=n;i++) s[i] = i;
		for(int i = 1;i<=m;i++) {
			int fx = find(find,e[i][0]),fy = find(find,e[i][1]);
			if(fx ^ fy) s[fx] = fy;
			else u = e[i][0],v = e[i][1];
		}
		for(int i = 1;i<=m + m;i++) {
			if((e[i][0] == u && e[i][1] == v) || (e[i][1] == u && e[i][0] == v)) continue;
			add(e[i][0],e[i][1]);
		}
		dfs1(1,0);dfs2(1,1);
		addvis(u,v);
		init();
		sort(e + 1,e + m + m + 1,[&](array<int,2> x,array<int,2> y) {
			if(x[0] == y[0]) return x[1] > y[1];
			return x[0] < y[0];
		});
		init();
		for(int i = 1;i<=m + m;i++) {
			if((e[i][0] == u && e[i][1] == v) || (e[i][1] == u && e[i][0] == v)) continue;
			add(e[i][0],e[i][1]);
		}
		dfs(1,0);
		for(int x = n;x>=1;x--) {
			if(vis[x] && vis[fa[x]])
				u = x,v = fa[x];
			else continue;
			init();
			for(int i = 1;i<=m + m;i++) {
				if((e[i][0] == u && e[i][1] == v) || (e[i][1] == u && e[i][0] == v)) continue;
				add(e[i][0],e[i][1]);
			}
			vector<int> ab = ans;
			ans.clear();
			auto cmp = [&](vector<int> a,vector<int> b) -> bool{
				for(int i = 0;i<a.size();i++) 
					if(a[i] < b[i]) return true;
					else if(a[i] > b[i]) return false;
				return false;
			};
			dfs(1,0);
			if(cmp(ab,ans)) ans = ab;
		}
	}
	for(int i : ans) wt(i),putchar(' ');
	
	return 0;
}

作者:MingJunYi

出处:https://www.cnblogs.com/WG-MingJunYi/p/18400869

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   MingJunYi  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示