2024-01-12 训练总结

发布时间 2024-01-13 08:19:18作者: ZnPdCo

孤注一掷没成功。

T1 宝藏

[NOIP2017 提高组] 宝藏

题目背景

NOIP2017 D2T2

题目描述

参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 \(n\) 个深埋在地下的宝藏屋, 也给出了这 \(n\) 个宝藏屋之间可供开发的 \(m\) 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。

小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。

新开发一条道路的代价是 \(\mathrm{L} \times \mathrm{K}\)。其中 \(L\) 代表这条道路的长度,\(K\) 代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。

输入格式

第一行两个用空格分离的正整数 \(n,m\),代表宝藏屋的个数和道路数。

接下来 \(m\) 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏屋的编号(编号为 \(1-n\)),和这条道路的长度 \(v\)

输出格式

一个正整数,表示最小的总代价。

样例 #1

样例输入 #1

4 5 
1 2 1 
1 3 3 
1 4 1 
2 3 4 
3 4 1

样例输出 #1

4

样例 #2

样例输入 #2

4 5 
1 2 1 
1 3 3 
1 4 1 
2 3 4 
3 4 2

样例输出 #2

5

提示

【样例解释 \(1\)

小明选定让赞助商打通了 \(1\) 号宝藏屋。小明开发了道路 \(1 \to 2\),挖掘了 \(2\) 号宝藏。开发了道路 \(1 \to 4\),挖掘了 \(4\) 号宝藏。还开发了道路 \(4 \to 3\),挖掘了 \(3\) 号宝藏。

工程总代价为 $1 \times 1 + 1 \times 1 + 1 \times 2 = 4 $。

【样例解释 \(2\)

小明选定让赞助商打通了 \(1\) 号宝藏屋。小明开发了道路 \(1 \to 2\),挖掘了 \(2\) 号宝藏。开发了道路 \(1 \to 3\),挖掘了 \(3\) 号宝藏。还开发了道路 \(1 \to 4\),挖掘了 \(4\) 号宝藏。

工程总代价为 \(1 \times 1 + 3 \times 1 + 1 \times 1 = 5\)

【数据规模与约定】

对于 $ 20%$ 的数据: 保证输入是一棵树,\(1 \le n \le 8\)\(v \le 5\times 10^3\) 且所有的 \(v\) 都相等。

对于 \(40\%\) 的数据: \(1 \le n \le 8\)\(0 \le m \le 10^3\)\(v \le 5\times 10^3\) 且所有的 \(v\) 都相等。

对于 $ 70%$ 的数据: \(1 \le n \le 8\)\(0 \le m \le 10^3\)\(v \le 5\times 10^3\)

对于 $ 100%$ 的数据: \(1 \le n \le 12\)\(0 \le m \le 10^3\)\(v \le 5\times 10^5\)


\(\text{upd 2022.7.27}\):新增加 \(50\) 组 Hack 数据。

赛时想到可以使用 \(f_{s,i,j}\) 存对于打通状态为 \(s\),第 \(i\) 个节点深度为 \(j\) 的最短情况。

其实不需要存储每一个点,这对于答案是没有影响的,只需要存储根节点为 \(i\) 且深度为 \(j\) 的情况即可。

\(f_{s,i,j}=f_{s_1,i,j}+f_{s_2,i,j+1}+dis\times i\)

其中 \(i,j\in s,i\in s_1,j\in s_2,s_1\or s_2=s,s_1\and s_2=\empty\)

这份代码赛时足足想了很久,连随机化或剪枝都没打。

时间复杂度 \(O(n^32^n)\),剪枝之后可以过。

/**
 * @file 宝藏.cpp 
 * @tag: #GMOJ#dp#状压dp
 * @author: ZnPdCo
 * @date: 2024-01-13
 * @problem: https://gmoj.net/senior/#main/show/5477
 **/
#include <cstdio>
#include <algorithm>
#include <cstring>
#define ll long long
#define N 13
using namespace std;
ll n, m;
ll road[N][N];
ll f[1<<N][N][N];
ll ans = 1e15;
// 子集状态 根 深度
int main() {
	freopen("treasure.in", "r", stdin);
	freopen("treasure.out", "w", stdout);
	scanf("%lld %lld", &n, &m);
	for(ll i = 1; i <= n; i++) {
		for(ll j = i+1; j <= n; j++) {
			road[i][j] = road[j][i] = 1e15;
		}
	}
	for(ll i = 1; i <= m; i++) {
		ll u, v, w;
		scanf("%lld %lld %lld", &u, &v, &w);
		if(w < road[u][v]) {
			road[u][v] = road[v][u] = w;
		}
	}
	for(ll s = 0; s < (1<<n); s++) {
		for(ll i = 1; i <= n; i++) {
			for(ll j = 1; j <= n; j++) {
				f[s][i][j] = 1e15;
			}
		}
	}
	for(ll i = 1; i <= n; i++) {
		for(ll j = 1; j <= n; j++) {
			f[1<<(i-1)][i][j] = 0;
		}
	}
	for(ll s = 0; s < (1<<n); s++) {		// 包含i、j的集
		for(ll ss = 0; ss < (1<<n); ss++) {		// 不包含i、包含j的s的子集
			if((ss & s) != ss) continue;
			for(ll i = 1; i <= n; i++) {				// 根
				if(!(s & (1<<(i-1)))) continue;
				if(ss & (1<<(i-1))) continue;
				for(ll j = 1; j <= n; j++) {			// 根的儿子
					if(!(s & (1<<(j-1)))) continue;
					if(!(ss & (1<<(j-1)))) continue;
					if(i == j || road[i][j] == 1e15) continue;
					for(ll d = 1; d < n; d++) {		// 深度
						f[s][i][d] = min(f[s][i][d], f[s ^ ss][i][d] + f[ss][j][d+1] + road[i][j] * d);
					}
				}
			}
		}
	}
	
	for(ll i = 1; i <= n; i++) {
		for(ll j = 1; j <= n; j++) {
			ans = min(ans, f[(1<<n)-1][i][j]);
		}
	}
	printf("%lld", ans);
}

T2 列队

[NOIP2017 提高组] 列队

题目背景

NOIP2017 D2T3

题目描述

Sylvia 是一个热爱学习的女孩子。

前段时间,Sylvia 参加了学校的军训。众所周知,军训的时候需要站方阵。

Sylvia 所在的方阵中有 \(n \times m\) 名学生,方阵的行数为 \(n\),列数为 \(m\)

为了便于管理,教官在训练开始时,按照从前到后,从左到右的顺序给方阵中 的学生从 \(1\)\(n \times m\) 编上了号码(参见后面的样例)。即:初始时,第 \(i\) 行第 \(j\) 列 的学生的编号是 \((i-1)\times m + j\)

然而在练习方阵的时候,经常会有学生因为各种各样的事情需要离队。在一天 中,一共发生了 \(q\) 件这样的离队事件。每一次离队事件可以用数对 \((x,y) (1 \le x \le n, 1 \le y \le m)\) 描述,表示第 \(x\) 行第 \(y\) 列的学生离队。

在有学生离队后,队伍中出现了一个空位。为了队伍的整齐,教官会依次下达 这样的两条指令:

  1. 向左看齐。这时第一列保持不动,所有学生向左填补空缺。不难发现在这条 指令之后,空位在第 \(x\) 行第 \(m\) 列。
  2. 向前看齐。这时第一行保持不动,所有学生向前填补空缺。不难发现在这条 指令之后,空位在第 \(n\) 行第 \(m\) 列。

教官规定不能有两个或更多学生同时离队。即在前一个离队的学生归队之后, 下一个学生才能离队。因此在每一个离队的学生要归队时,队伍中有且仅有第 \(n\) 行 第 \(m\) 列一个空位,这时这个学生会自然地填补到这个位置。

因为站方阵真的很无聊,所以 Sylvia 想要计算每一次离队事件中,离队的同学 的编号是多少。

注意:每一个同学的编号不会随着离队事件的发生而改变,在发生离队事件后 方阵中同学的编号可能是乱序的。

输入格式

输入共 \(q+1\) 行。

第一行包含 \(3\) 个用空格分隔的正整数 \(n, m, q\),表示方阵大小是 \(n\)\(m\) 列,一共发 生了 \(q\) 次事件。

接下来 \(q\) 行按照事件发生顺序描述了 \(q\) 件事件。每一行是两个整数 \(x, y\),用一个空 格分隔,表示这个离队事件中离队的学生当时排在第 \(x\) 行第 \(y\) 列。

输出格式

按照事件输入的顺序,每一个事件输出一行一个整数,表示这个离队事件中离队学生的编号。

样例 #1

样例输入 #1

2 2 3 
1 1 
2 2 
1 2

样例输出 #1

1
1
4

提示

【输入输出样例 \(1\) 说明】

\[\begin{matrix} \begin{bmatrix} 1 & 2 \\ 3 & 4 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} & 2 \\ 3 & 4 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & \\ 3 & 4 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & 1 \\ \end{bmatrix} \\[1em] \begin{bmatrix} 2 & 4 \\ 3 & 1 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 4 \\ 3 & 1 \\ \end{bmatrix}\\[1em] \begin{bmatrix} 2 & 4 \\ 3 & 1 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & \\ 3 & 1 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & \\ 3 & 1 \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 1 \\ 3 & \\ \end{bmatrix} & \Rightarrow & \begin{bmatrix} 2 & 1 \\ 3 & 4 \\ \end{bmatrix} \end{matrix}\]

列队的过程如上图所示,每一行描述了一个事件。 在第一个事件中,编号为 \(1\) 的同学离队,这时空位在第一行第一列。接着所有同学 向左标齐,这时编号为 \(2\) 的同学向左移动一步,空位移动到第一行第二列。然后所有同 学向上标齐,这时编号为 \(4\) 的同学向上一步,这时空位移动到第二行第二列。最后编号为 \(1\) 的同学返回填补到空位中。

【数据规模与约定】

测试点编号 \(n\) \(m\) \(q\) 其他约定
\(1\sim 6\) \(\le 10^3\) \(\le 10^3\) \(\le 500\)
\(7\sim 10\) \(\le 5\times 10^4\) \(\le 5\times 10^4\) \(\le 500\)
\(11\sim 12\) \(=1\) \(\le 10^5\) \(\le 10^5\) 所有事件 \(x=1\)
\(13\sim 14\) \(=1\) \(\le 3\times 10^5\) \(\le 3\times 10^5\) 所有事件 \(x=1\)
\(15\sim 16\) \(\le 3\times 10^5\) \(\le 3\times 10^5\) \(\le 3\times 10^5\) 所有事件 \(x=1\)
\(17\sim 18\) \(\le 10^5\) \(\le 10^5\) \(\le 10^5\)
\(19\sim 20\) \(\le 3\times 10^5\) \(\le 3\times 10^5\) \(\le 3\times 10^5\)

数据保证每一个事件满足 \(1 \le x \le n,1 \le y \le m\)

其实是平衡树,但我忘记平衡树怎么打了。

这种基础应该牢记在心。

最后考虑我熟悉的权值线段树,直接把剔出的再用一个数据结构维护,因为剔出的不多,可以做到 \(\log\) 级别查询。

/**
 * @file 列队.cpp 
 * @tag: #GMOJ#数据结构#ds
 * @author: ZnPdCo
 * @date: 2024-01-13
 * @problem: https://gmoj.net/senior/#main/show/5475
 **/
#include <cstdio>
#include <algorithm>
#include <vector>
#define ll long long
#define N 1000010
using namespace std;
ll n, m, q, mx;
ll rt[N], cnt;
vector <ll> v[N];
struct node {
	ll ls, rs, mov;
	// mov是删掉了多少个(存区间个数还要初始化,我懒)
} t[4*N];
ll query(ll x, ll l, ll r, ll pos) {
	if(l == r) return l;
	ll mid = (l + r) >> 1;
	ll num = mid - l + 1 - t[t[pos].ls].mov;
	if(x <= num) {
		return query(x, l, mid, t[pos].ls);
	} else {
		return query(x-num, mid+1, r, t[pos].rs);
	}
}
void update(ll x, ll l, ll r, ll &pos, ll k) {
	if(!pos) pos = ++cnt;
	t[pos].mov += k;
	if(l == r) return;
	ll mid = (l + r) >> 1;
	if(x <= mid) {
		update(x, l, mid, t[pos].ls, k);
	} else {
		update(x, mid+1, r, t[pos].rs, k);
	}
}
// 向前看齐
ll front(ll x, ll add) {
	ll res = query(x, 1, mx, rt[n+1]);
	update(res, 1, mx, rt[n+1], 1);
	ll id;
	if(res <= n) {
		id = res * m;
	} else {
		id = v[n+1][res - n - 1];
	}
	if(add) v[n+1].push_back(add);
	else v[n+1].push_back(id);
	return id;
}
// 向左看齐
ll left(ll x, ll y) {
	ll res = query(y, 1, mx, rt[x]);
	update(res, 1, mx, rt[x], 1);
	ll id;
	if(res < m) {
		id = (x - 1) * m + res;
	} else {
		id = v[x][res - m];
	}
	v[x].push_back(front(x, id));
	return id;
}
int main() {
//	freopen("phalanx.in", "r", stdin);
//	freopen("phalanx.out", "w", stdout);
	scanf("%lld %lld %lld", &n, &m, &q);
	mx = max(n, m) + q;
	for(ll i = 1; i <= q; i++) {
		ll x, y;
		scanf("%lld %lld", &x, &y);
		if(y == m) printf("%lld\n", front(x, 0));
		else printf("%lld\n", left(x, y));
	}
}

T3 逛公园

[NOIP2017 提高组] 逛公园

题目背景

NOIP2017 D1T3

题目描述

策策同学特别喜欢逛公园。公园可以看成一张 \(N\) 个点 \(M\) 条边构成的有向图,且没有 自环和重边。其中 \(1\) 号点是公园的入口,\(N\) 号点是公园的出口,每条边有一个非负权值, 代表策策经过这条边所要花的时间。

策策每天都会去逛公园,他总是从 \(1\) 号点进去,从 \(N\) 号点出来。

策策喜欢新鲜的事物,它不希望有两天逛公园的路线完全一样,同时策策还是一个 特别热爱学习的好孩子,它不希望每天在逛公园这件事上花费太多的时间。如果 \(1\) 号点 到 \(N\) 号点的最短路长为 \(d\),那么策策只会喜欢长度不超过 \(d + K\) 的路线。

策策同学想知道总共有多少条满足条件的路线,你能帮帮它吗?

为避免输出过大,答案对 \(P\) 取模。

如果有无穷多条合法的路线,请输出 \(-1\)

输入格式

第一行包含一个整数 \(T\), 代表数据组数。

接下来 \(T\) 组数据,对于每组数据: 第一行包含四个整数 \(N,M,K,P\),每两个整数之间用一个空格隔开。

接下来 \(M\) 行,每行三个整数 \(a_i,b_i,c_i\),代表编号为 \(a_i,b_i\) 的点之间有一条权值为 \(c_i\) 的有向边,每两个整数之间用一个空格隔开。

输出格式

输出文件包含 \(T\) 行,每行一个整数代表答案。

样例 #1

样例输入 #1

2
5 7 2 10
1 2 1
2 4 0
4 5 2
2 3 2
3 4 1
3 5 2
1 5 3
2 2 0 10
1 2 0
2 1 0

样例输出 #1

3
-1

提示

【样例解释1】

对于第一组数据,最短路为 \(3\)\(1\to 5, 1\to 2\to 4\to 5, 1\to 2\to 3\to 5\)\(3\) 条合法路径。

【测试数据与约定】

对于不同的测试点,我们约定各种参数的规模不会超过如下

测试点编号 \(T\) \(N\) \(M\) \(K\) 是否有 \(0\)
\(1\) \(5\) \(5\) \(10\) \(0\)
\(2\) \(5\) \(10^3\) \(2\times 10^3\) \(0\)
\(3\) \(5\) \(10^3\) \(2\times 10^3\) \(50\)
\(4\) \(5\) \(10^3\) \(2\times 10^3\) \(50\)
\(5\) \(5\) \(10^3\) \(2\times 10^3\) \(50\)
\(6\) \(5\) \(10^3\) \(2\times 10^3\) \(50\)
\(7\) \(5\) \(10^5\) \(2\times 10^5\) \(0\)
\(8\) \(3\) \(10^5\) \(2\times 10^5\) \(50\)
\(9\) \(3\) \(10^5\) \(2\times 10^5\) \(50\)
\(10\) \(3\) \(10^5\) \(2\times 10^5\) \(50\)

对于 \(100\%\) 的数据,\(1 \le P \le 10^9\)\(1 \le a_i,b_i \le N\)\(0 \le c_i \le 1000\)

数据保证:至少存在一条合法的路线。


  • 2019.8.30 增加了一组 hack 数据 by @skicean
  • 2022.7.21 增加了一组 hack 数据 by @djwj233

赛时孤注一掷了这题没有看。

可以想到最短路,然后再从终点分 \(k\) 种状态跑回来用记忆化搜索即可。

如果是记忆化搜索的话找环只用看一个节点的一种状态有没有跑过两次,如果有说明可以无限的跑。

/**
 * @file 逛公园.cpp 
 * @tag: #GMOJ#图论#最短路
 * @author: ZnPdCo
 * @date: 2024-01-13
 * @problem: https://gmoj.net/senior/#main/show/5475
 **/
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
#define ll long long
#define N 100010
#define M 200010
#define K 100
ll he1[N];
ll he2[N];
ll nxt[2*M];
ll cost[2*M];
ll to[2*M], cnt;
ll T;
ll n, m, k, p;

void addEdge(ll head[], ll u, ll v, ll w) {
	++cnt;
	to[cnt] = v;
	nxt[cnt] = head[u];
	cost[cnt] = w;
	head[u] = cnt;
}

bool vis[N];
ll dis[N];
priority_queue<pair<ll, ll> > q;
void dij() {
	memset(vis, 0, sizeof vis);
	dis[1] = 0;
	for(ll i = 2; i <= n; i++) dis[i] = 1e15;
	q.push(make_pair(0, 1));
	while(!q.empty()) {
		ll u = q.top().second;
		q.pop();
		if(vis[u]) continue;
		vis[u] = 1;
		for(ll i = he1[u]; i; i = nxt[i]) {
			ll v = to[i];
			if(!vis[v]) {
				if(dis[v] > dis[u] + cost[i]) {
					dis[v] = dis[u] + cost[i];
					q.push(make_pair(-dis[v], v));
				}
			}
		}
	}
}

ll f[N][K];
bool flag[N][K];

ll dfs(ll u, ll x) {
	if(x < 0 || x > k) return 0;
	if(flag[u][x]) return -1;
	if(f[u][x] != -1) return f[u][x];
	flag[u][x] = 1;
	f[u][x] = 0;
	for(ll i = he2[u]; i; i = nxt[i]) {
		ll v = to[i];
		ll res = dfs(v, x - ((dis[v] + cost[i]) - dis[u]));
		if(res == -1) return -1;
		(f[u][x] += res) %= p;
	}
	if(u == 1 && x == 0) {
		(f[u][x] += 1) %= p;
	}
	flag[u][x] = 0;
	return f[u][x];
}

int main() {
//	freopen("park.in", "r", stdin);
//	freopen("park.out", "w", stdout);
	scanf("%lld", &T);
	while(T--) {
		memset(he1, 0, sizeof he1);
		memset(he2, 0, sizeof he2);
		cnt = 0;
		scanf("%lld %lld %lld %lld", &n, &m, &k, &p);
		for(ll i = 1; i <= m; i++) {
			ll u, v, w;
			scanf("%lld %lld %lld", &u, &v, &w);
			addEdge(he1, u, v, w);
			addEdge(he2, v, u, w);
		}
		dij();
		for(ll i = 1; i <= n; i++) {
			for(ll j = 0; j <= k; j++) {
				flag[i][j] = 0;
				f[i][j] = -1;
			}
		}
		ll ans = 0;
		bool isans = true;
		for(ll i = 0; i <= k; i++) {
			ll res = dfs(n, i);
			if(res == -1) isans = false;
			else (ans += res) %= p;
		}
		if(isans) printf("%lld\n", ans);
		else printf("-1\n");
	}
}

T4 王国内战·剑鬼·魔法阵

智障oj不给复制markdown

Problem Description

【题目背景】

「鬼……」
鬼。没错,是鬼。在那边的是正在挥剑的鬼。在战场上砍杀敌人、笑得开怀的鬼。
享受挥剑杀戮的乐趣,像玩乐一样量产死亡的鬼。
——亦即,剑鬼。
「来啊!再来啊!尽管过来!放马过来!我会杀了你们,然后活下去!!」
剑鬼叫喊,每一次的银闪都夺去亚人族的性命。
面前是那样的惨状,格林慢慢转过头,俯瞰山丘下的光景。
在烟雾弥漫的战场上,只有魔法阵的淡淡光芒鲜明异常。
用上双手双脚的指头也数不清的魔法阵,在战场上注入超越人类智慧的力量,以破坏力粉碎愚蠢地被围困的王国军。
热风,地鸣,还有远方被烧红的天空中所回荡的遍野哀嚎——
「……这就是我做的选择的下场吗?」
背后是剑鬼制造的尸山血河。
眼底是被遗弃的同伴的惨叫与叫喊,听来像在诅咒生者。
不知何时格林跪地,双手掩面不停地啜泣。
「对、不起……对不起,对不起,对不起……!」
直至战斗结束,王国军吞下败战苦果并回收友军之前,格林带着哽咽的谢罪以及剑鬼的剑舞和大笑声,都没有中断过。
——撤退的小队以及托尔塔·威兹利都没有回来。

【题目描述】

「……原来如此。听到的时候就有想到会是这样,这果然是花了相当多的时间来设置的术式。不管是拟定战术的人还是执行的人,要是不精通魔法,可办不到 ~ 呢。这样看来,王国恐怕是岌岌可危。」
「是、是这样吗?这是那么危险的魔法……」
「这个单一魔法阵本身的危险性就不用说了,问题在于敌方阵营里头出类拔萃的魔法使者不只——一人。在整个战场铺设魔法阵,从构想来看就已经很反常了……但同样的手法,也就可以套用在其他地方——啰。」

……

经过宫廷魔导师罗兹瓦尔·J·梅札斯的分析,所有的魔法阵都按照如下规则布置:魔法阵中有 n 个节点和 n 条边,边有边权,一开始每条边是暗淡的,边权是边点亮后能够吸收的玛那量。这 n 条边都可以用 (i,t**i,v**i) 表示,即 it**i 的一条双向边,权值为 v**i。可能会出现重边。为了方便记所有边和点形成的图为 F,亮着的边和点形成的图为 G

现在需要破解这个魔法阵:对于一个魔法阵,王国军队需要派出一名优秀的魔法使者来对付。亚人军队也有操控这个魔法阵的使者。在启动前,王国军队需要尽快施展魔法操作魔法阵,使得最终魔法阵的破坏力最小;当然对方也会尽力阻挠,使最终的魔法阵的破坏力最大。由于操作魔法需要时间间隔,在最快速度下两人刚好轮流施展魔法,王国军队为先手。不管是哪边,施展魔法时,每次选择一条合法的边,将其点亮。某条边合法的条件当且仅当:这条边没有被点亮,且如果点亮这条边,G 中不会出现环。如果不存在可以选择的边,魔法阵启动,产生的破坏力和它吸收的玛那量成正比。

双方的魔法师都是绝顶聪明的,所以罗兹瓦尔想要知道,对于一个特定的魔法阵,最终它吸收的玛那量为多少,以判断这个魔法阵的危险程度。

Input

从文件 magic.in 中读入数据。

第一行一个整数 n

第二行 n 个整数,表示数组 t**i

第三行 n 个整数,表示数组 v**i

数据不保证无自环

Output

输出到文件 magic.out 中。

一行一个整数表示答案。

Sample Data

Sample #1

见下发文件。

Sample #2

见下发文件。

Sample #3

见下发文件。

Data Constraint

数据点编号 分值 n v**i 特殊性质
1 5 10 10
2 10 20 20
3 10 100 106 F 由两个环组成
4 15 105 106 F 中的所有环长度为奇数
5 15 105 106 F 中的所有环长度为 2
6 45 105 106

对于所有的数据,保证 n≤105,v**i≤106,t**i∈[1,n]。

赛时孤注一掷了这题也没有看。

其实是非常简单的结论题,发现图其实是基环森林,树边一定是可以选的,对于奇数环边两人肯定是一大一小地选,最后剩下中位数。对于偶数环边假如 \(l<r\),如果要选 \(i\) 环,先手肯定选 \(l_i+r_j\le l_j+r_i\)\(l_i-r_i\) 较小的,后手肯定选 \(r_i+l_j\ge r_j+l_i\)\(l_i-r_i\) 也较小的。

排序处理即可。

/**
 * @file 王国内战·剑鬼·魔法阵.cpp 
 * @tag: #GMOJ#结论#图论
 * @author: ZnPdCo
 * @date: 2024-01-13
 * @problem: https://gmoj.net/senior/#main/show/6958
 **/
#include <cstdio>
#include <algorithm>
#define ll long long
#define N 100010
using namespace std;
ll n, ans;
ll t[N];
ll val[N];

ll head[N];
ll nxt[2*N];
ll cost[2*N];
ll to[2*N], cnt = 1;
bool sta = 0;
void addEdge(ll u, ll v, ll w) {
	++cnt;
	to[cnt] = v;
	cost[cnt] = w;
	nxt[cnt] = head[u];
	head[u] = cnt;
}


ll dfn[N], ti;

ll co[N], tot;

struct node {
	ll l, r;	// l较小,r较大
	// 先手要选择li,则li+rj<=lj+ri,即使得li-ri尽量小
	// 后手要选择ri,则ri+lj>=rj+li,即使得li-ri尽量小
} even[N];		// 存储所有偶数环的l-r
ll num;

ll dfs(ll u, ll lst=0) {
	dfn[u] = ti;
	for(ll i = head[u]; i; i = nxt[i]) {
		ll v = to[i];
		if((i ^ 1) == lst) continue;		// 不能走同一条边
		if(!dfn[v]) {
			ll res = dfs(v, i);
			if(res) {
				co[++tot] = cost[i]; 
				ans -= cost[i];
				if(res == u) {
					ti++;
					return 0;
				}
				return res;
			}
		} else if(dfn[v] == ti) {
			co[++tot] = cost[i]; 
			ans -= cost[i];
			if(v == u) {
				ti++;
				return 0;
			}
			return v;
		}
	}
	return 0;
}
bool cmp(node x, node y) {
	return x.l-x.r<y.l-y.r;
}
int main() {
	freopen("magic.in", "r", stdin);
	freopen("magic.out", "w", stdout);
	scanf("%lld", &n);
	for(ll i = 1; i <= n; i++) {
		scanf("%lld", &t[i]);
	}
	for(ll i = 1; i <= n; i++) {
		scanf("%lld", &val[i]);
		addEdge(i, t[i], val[i]);
		addEdge(t[i], i, val[i]);
		ans += val[i];
	}
	for(ll i = 1; i <= n; i++) {
		if(!dfn[i]) {
			ti++;
			tot = 0;
			dfs(i);
			if(!tot) continue;
			sort(co+1, co+1+tot);
			if(tot % 2 == 1) {					// 奇数情况不选择中位数
				for(ll j = 1; j <= tot; j++) if(j != tot / 2 + 1) {
					ans += co[j];
				}
			} else {							// 偶数情况选择l-r最小
				for(ll j = 1; j <= tot; j++) if(j != tot / 2 && j != tot / 2 + 1) {
					ans += co[j];
				}
				even[++num].l = co[tot/2];
				even[num].r = co[tot/2+1];
			}
		}
	}
	sort(even+1, even+1+num, cmp);
	sta = 0;
	for(ll i = 1; i <= num; i++) {
		if(!sta) {
			ans += even[i].l;
		} else {
			ans += even[i].r;
		}
		sta = !sta;
	}
	printf("%lld", ans);
}

总的来说,孤注一掷不是一种好习惯,在这次训练中,我孤注一掷在 T1、T2,如果题目顺序不是正序,那么这种想法是非常不好的。

虽然有几次孤注一掷成功,但是还是要定时做题,把每一题都看一遍好呀。