2023 ICPC HIAST Collegiate Programming Contest

发布时间 2023-10-05 16:30:10作者: jackle

因为补题的时候,发现网上找不到一篇题解(补题补的很是痛苦),所以写了一篇,希望能帮助之后补这场比赛的人~~~
有些太简单签到就没写,还有 \(2-3\) 题还没补出来,之后补了会加上去。

A. Gym Plates

解题思路

比较裸的一个状压 DP,我们考虑把数字的选取次数压到 DP 里面去,显然 \(0\backsim 9\) 这些数的状态会构成一个 \(10\) 位的 \(3\) 进制数。然后正常转移即可:

\[dp[i+1][state] = \max(dp[i + 1][state], dp[i][j] + w[i + 1]) \]

\[dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]) \]

转移的时候维护好 \(state\) 的合法性即可。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int pw[20];

int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	cin >> t;
	pw[0] = 1;
	for (int i = 1; i <= 10; i++) {
		pw[i] = pw[i - 1] * 3;
	}
	while (t--) {
		int n;
		cin >> n;
		vector<LL> w(n + 1);
		for (int i = 1; i <= n; i++) {
			cin >> w[i];
		}
		vector<LL> dp(pw[10], -1);
		dp[0] = 0;
		for (int i = 0; i < n; i++) {
			vector<LL> ndp(pw[10], -1);
			for (int j = 0; j < pw[10]; j++) {
				if (dp[j] == -1) continue;
				ndp[j] = max(ndp[j], dp[j]);
				vector<int> cnt(10, 0);
				LL v = w[i + 1];
				while (v) {
					cnt[v % 10]++;
					v /= 10;
				}
				int state = 0;
				bool ok = true;
				for (int k = 9; k >= 0; k--) {
					int p = j / pw[k] % 3;
					if (p + cnt[k] > 2) {
						ok = false;
						break;
					}
					state = state * 3 + p + cnt[k];
				}
				if (ok) {
					ndp[state] = max(ndp[state], dp[j] + w[i + 1]);
				}
			}
			dp = ndp;
		}
		cout << *max_element(dp.begin(), dp.end()) << "\n";
	}
	
	return 0;
} 

B. Convarge To 1

解题思路

首先我们设 \(t_i\) 表示每个位置的数第一次变为 \(1\) 的操作次数,那么对于一个查询 \([l,r]\),这个区间全变成 \(1\) 的最早操作时间 \(\iff\) \(\max_{l\leq i \leq r}t_i\)。问题就变成一个静态区间查询最大值问题了,可以采用 \(ST\) 表解决。

然后问题变成了我们如何求出每一个 \(t_i\),如果根据题目的模拟,我们不难想到预处理出每个数的最大质因子,然后还要利用堆,我们想一下最坏复杂度是多少,每个数最多会被除 \(20\) 多次,加上堆的 \(\log\),我们复杂度是 \(O(n\log^2 n)\),显然不行。

我们考虑优化堆的这个 \(\log\),我们可以采用递推的方法,因为我们的数是越来越小的,我们可以倒过来做,用一个 \(g[i]\) 维护值为 \(i\) 的所有数,然后它们都会进行操作后变到 \(g[i/mip_i]\) 里。

而且我们发现,当一个数变为质数了,那么下一次它一步一定会变到 \(1\),那么当 \(i\) 不是质数,其实我们并不需要管里面数的顺序,因为它们整体的时间都要加上 \(g[i].size()\)。只有当 \(i\) 为质数了,说明它下一步要变成 \(1\) 了, 我们在考虑它的顺

序,此时我们在对 \(g[i]\) 按下标排序。

我们来分析一下这个复杂度,首先我们不管排序,我们知道每个数最多被除 \(\log\) 多次,那么相当于我们每个数最多被遍历 \(\log\) 次,故这一块的复杂度是 \(O(n\log n)\),我们在考虑排序,我们只有当 \(i\) 是质数的时候才去排序,而每一个数最多只会

在自身变成质数的时候才会进行排序,所以我们知道了每个数最多被排序一次,所以这一块的复杂度也是 \(O(n\log n)\) 的,所以我们总体复杂度就是 \(O(n\log n)\)的。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e6 + 10;
int mip[N], a[N], t[N], lg[N];
int f[N][22];
vector<int> g[N];

void init(int n) {
	for (int i = 2; i <= n; i++) {
		if (!mip[i]) {
			mip[i] = i;
			for (int j = 2 * i; j <= n; j += i) {
				mip[j] = i;
			}
		}
	}
}

void init_st(int n) {
	for (int j = 0; j <= lg[n]; j++) {
		for (int i = 1; i + (1 << j) - 1 <= n; i++) {
			if (!j) f[i][j] = t[i];
			else f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
		}
	}
}

int query(int l, int r) {
	int k = lg[r - l + 1];
	return max(f[l][k], f[r - (1 << k) + 1][k]);
}


int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int n, q;
	cin >> n >> q;
	int mx = 0;
	for (int i = 1; i <= n; i++) {
		if (i > 1) lg[i] = lg[i / 2] + 1;
		cin >> a[i];
		mx = max(mx, a[i]);	
		g[a[i]].push_back(i);
	}
	init(mx);
	int tot = 0;
	for (int i = mx; i >= 2; i--) {
		if (g[i].size() == 0) continue;
		if (mip[i] == i) {
			sort(g[i].begin(), g[i].end());
		}
		for (auto v : g[i]) {
			tot++;
			if (mip[i] == i) t[v] = tot;
			else {
				g[i / mip[i]].push_back(v);
			}
		}
	}
	
	init_st(n);
	while (q--) {
		int l, r;
		cin >> l >> r;
		cout << query(l, r) << "\n";
	}
	
	return 0;
} 

C. Tree Permutation

解题思路

首先这类期望题,一眼看上去就是 DP 不了的,我们考虑推公式或者直接算贡献。

我们发现答案其实就是 \(\frac{sum}{n!}\),其中 \(sum\) 是所有情况下所走的路径和。

我们考虑一下如何计算 \(sum\),我们可以考树上任意一个点对 \((u,v)\) 对答案产生的贡献:如果 \(u\rightarrow v\),那么 \(u,v\) 填的数必须是如下几种情况之一:

  • \((1,2)\)
  • \((2,3)\)
  • \(.....\)
  • \((n-1,n)\)

那么剩下的 \(n-2\) 个位置还有 \((n-2)!\) 中排列方式,那么这 \((u,v)\) 点对产生的贡献就是:\(dis(u,v)\times (n-1)\times (n-2)!=dis(u,v)\times (n-1)!\)

故求 \(\frac{sum}{n!}\iff \frac{\sum_{i=1}^n\sum_{j=1}^ndis(i,j)\times (n-1)!}{n!}=\frac{\sum_{i=1}^n\sum_{j=1}^ndis(i,j)}{n}\)

所以问题被转化为一个经典的问题,求树上任意两点的距离之和,这是一个经典的换根 DP。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
vector<int> g[N];
LL dp[N];
int sz[N];


void dfs1(int u, int fa) {
	dp[u] = 0, sz[u] = 1;
	for (auto v : g[u]) {
		if (v == fa) continue;
		dfs1(v, u);
		sz[u] += sz[v];
		dp[u] += dp[v];
	}
	dp[u] += sz[u] - 1;
}

void dfs2(int u, int fa) {
	
	for (auto v : g[u]) {
		if (v == fa) continue;
		dp[v] = dp[u] + sz[1] - 2 * sz[v];
		dfs2(v, u);
	}
}

int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	cin >> t;
	while (t--) {
		int n;
		cin >> n;
		for (int i = 1; i <= n; i++) g[i].clear();
		for (int i = 1; i < n; i++) {
			int u, v;
			cin >> u >> v;
			g[u].push_back(v);
			g[v].push_back(u);
		}
		dfs1(1, 0);
		dfs2(1, 0);
		double ans = 0;
		for (int i = 1; i <= n; i++) {
			ans += dp[i];
		} 
		cout << fixed << setprecision(12) << ans / n << "\n";
	} 
	
	return 0;
} 

H. Yaser In Baradah

解题思路

直接用 set + multiset 维护就好了。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	cin >> t;
	while (t--) {
		int n;
		cin >> n;
		set<int> pos;
		multiset<LL> val;
		vector<LL> vgn(n + 1);
		for (int i = 1; i <= n; i++) {
			int x;
			cin >> x;
			vgn[i] = x;
			pos.insert(i);
			val.insert(x);
		}
		cout << *val.rbegin() << "\n";
		int Q;
		cin >> Q;
		while (Q--) {
			int s;
			cin >> s;
			auto it = pos.upper_bound(s);
			vgn[*it] += vgn[s];
			pos.erase(pos.find(s));
			val.insert(vgn[*it]);
			cout << *val.rbegin() << "\n";
		}
	}
	
	return 0;
} 

J. Completely Balanced

解题思路

如果我们知道了数组最后的中位数,那么 \(X\) 是可以被算出来的:\(\frac{sum+X}{n+1}=mid\)
我们考虑加入一个数之后,数组的中位数可以是哪些数。

  • 是原先数组中的数:把数组排序,那么 \(a[(n+2) / 2]\)\(a[(n+2) / 2 - 1]\) 都有可能成为中位数。
  • 是新加入的那个数 \(X\)
    我们只要依次检查这三种情况,取最小的 \(ans\) 即可。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;


int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	cin >> t;
	while (t--) {
		int n;
		cin >> n;
		vector<LL> a(n + 1);
		LL sum = 0;
		for (int i = 1; i <= n; i++) {
			cin >> a[i];
			sum += a[i];
		}
		n += 1;
		sort(a.begin() + 1, a.end());
		// 1. 考虑中位数是原先的数字
		int mid = (n + 1) / 2;
		// 那么 x, a[mid] , a[mid - 1] 都有可能成为中位数 
		LL ans = 2e18;
		LL x1 = (n * a[mid] - sum);
		auto check = [&](LL x, LL y) {
			vector<LL> b = a;
			b.push_back(x);
			sort(b.begin() + 1, b.end());
			return b[mid] == y && (sum + x) % n == 0;	
		};
		if (check(x1, a[mid])) ans = min(ans, x1);
		if (mid - 1 >= 1) {
			LL x2 = (n * a[mid - 1] - sum);
			if (check(x2, a[mid - 1])) ans = min(ans, x2);
		}
		if (sum % (n - 1) == 0) {
			LL x = sum / (n - 1);
			if (check(x, x)) ans = min(ans, x);
		}
		cout << ans << "\n";
	}
	
	return 0;
} 

K. Sam-Oh, the funny coach

解题思路

我们可以对于每一个字符串都维护一个长度最多为 \(26\) 的数组,存放每种字母出现的区间。
询问的时候,依次让两个字符串所在的区间求交,即可算出答案。

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
vector<array<int, 3>> g[N];

int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		string s;
		cin >> s;
		for (int j = 0; j < m; j++) {
			int p = j;
			while (p < m && s[j] == s[p]) p++;
			g[i].push_back({j + 1, p, s[j] - 'a'});
			j = p - 1;
		}
	}
	int Q;
	cin >> Q;
	while (Q--) {
		int p1, p2;
		cin >> p1 >> p2;
		vector<array<int, 3>> &it1 = g[p1], &it2 = g[p2];
		vector<array<int, 2>> cur[26];
		for (auto &[l, r, t] : it1) {
			cur[t].push_back({l, r});
		}
		int ans = 0;
		for (auto &[l, r, t] : it2) {
			if (cur[t].size()) {
				int L = cur[t][0][0], R = cur[t][0][1];
				ans += max(0, min(R, r) - max(L, l) + 1);
			}
		}
		cout << ans << "\n";
	}
	
	return 0;
} 

L. Trip Discount

解题思路

首先考虑 \(m\) 次操作,本质是一个路径加,我们可以采用树上差分的方法,预处理出每个边被加的次数,那么最终我们每个边的最终花费就是 \(w\times d[i]\),其中 \(d[i]\) 表示这条边被加的次数。

现在问题变成了,我们要选择 \(k\) 个点,然后可以使得这 \(k\) 个点任意两个点的路径边权全置成 \(0\),考虑到 \(k\) 最大只有 \(1000\)。这类树上选点集的问题,我们可以想到用树上背包去做。

我们设 \(dp[i][j]\) 表示在以 \(i\) 为根的子树里面,选了 \(j\) 个点,可以免去去的最大边权贡献之和是多少。转移的时候,我们就类似树上背包的转移,枚举这棵子树选了几个点,依次合并其所有儿子的子树。

对于 \((u,v)\) 这条边要产生贡献,当且仅当 \(dp[v][j]\) 中的 \(j>0\)\(j\not= k\),这样一定会在 \(v\) 的子树外面选一个点,和 \(v\) 子树内有一个点,这两个点的路径一定会经过 \((u,v)\) 这条边。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<LL, int> PLI;
const int N = 1e4 + 10, M = 1010;
const LL INF = 1e18;
vector<array<int, 2>> g[N];
int dep[N], f[N][16], d[N], val[N], sz[N]; 
LL dp[N][M]; 
int n, k, m;

void dfs(int u, int fa) {
	dep[u] = dep[fa] + 1;
	for (auto &[v, w] : g[u]) {
		if (v == fa) continue;
		f[v][0] = u;
		val[v] = w;
		for (int i = 1; i <= 15; i++) {
			f[v][i] = f[f[v][i - 1]][i - 1];
		}
		dfs(v, u);
	}
}

void self(int u, int fa) {
	for (auto &[v, w] : g[u]) {
		if (v == fa) continue;
		self(v, u);
		d[u] += d[v];
	}
}

int LCA(int a, int b) {
	if (dep[a] < dep[b]) swap(a, b);
	for (int i = 15; i >= 0; i--) {
		if (dep[f[a][i]] >= dep[b]) {
			a = f[a][i];
		}
	}
	if (a == b) return a;
	for (int i = 15; i >= 0; i--) {
		if (f[a][i] != f[b][i]) {
			a = f[a][i];
			b = f[b][i];
		}
	}
	return f[a][0];
}


void DFS(int u, int fa) {
	sz[u] = 1;
	dp[u][0] = dp[u][1] = 0;
	for (auto [v, w] : g[u]) {
		if (v == fa) continue;
		DFS(v, u);
		LL cost = 1LL * d[v] * w;
		static LL tmp[M];
		for (int i = 0; i <= min(k, sz[u] + sz[v]); i++) tmp[i] = -INF;
		for (int i = 0; i <= min(k, sz[u]); i++) {
			for (int j = 0; j <= sz[v] && i + j <= k; j++) {
				tmp[i + j] = max(tmp[i + j], dp[u][i] + dp[v][j] + (j && j != k) * cost);
			}
		}
		sz[u] += sz[v];
		for (int i = 0; i <= min(k, sz[u]); i++) {
			dp[u][i] = tmp[i];
		}
	}
}

void solve() {
	cin >> n >> k >> m;
	for (int i = 1; i <= n; i++) g[i].clear(), d[i] = val[i] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= k; j++) {
			dp[i][j] = -INF;
		}
	}
	for (int i = 1; i < n; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		g[u].push_back({v, w});
		g[v].push_back({u, w});
	}
	dfs(1, 0);
	while (m--) {
		int u, v;
		cin >> u >> v;
		d[u]++, d[v]++, d[LCA(u, v)] -= 2;
	}
	self(1, 0);
	LL ans = 0;
	for (int i = 1; i <= n; i++) {
		ans += 1LL * val[i] * d[i];
	}
	DFS(1, 0);
	cout << ans - dp[1][k] << "\n";
}

int main() {
	cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	cin >> t;
	while (t--) {
		solve();
	}
	
	return 0;
}