OIFC 2025.11.25 模拟赛总结

生日当天还有模拟赛(

T1 Pocky游戏

题目描述

Mdk 和 Hmr 正在吃 Pocky,她们感到有些无聊,于是决定玩一个小游戏。

现在有一根长度为 \(n\) 的 Pocky,其中从左往右数第 \(i\) 单位长度 Pocky 的美味值为 \(a_i\)。现在 Mdk 和 Hmr 决定吃掉这根 Pocky,Mdk 从左往右吃,Hmr 从右往左吃。

  • Mdk 先开始吃,她会从左侧吃 \(1\)\(2\) 单位长度的 Pocky。

  • 接下来,如果上一个人吃了 \(k\) 单位的 Pocky,则下一个人可以吃 \(k\)\(k + 1\)(如果剩余长度 \(\geq k + 1\))单位的 Pocky。

  • 如果剩下的 Pocky 长度小于 \(k\),则这个游戏将会停止。

双方都想要最大化自己吃掉的 Pocky 的美味值减掉对方吃掉的 Pocky 的美味值,问在双方都使用最优策略的情况下,Mdk 吃掉的 Pocky 的美味值减去 Hmr 吃掉的 Pocky 的美味值将会是多少?

数据范围:\(1 \leq n \leq 4000, |a_i| \leq 10^4\)空间限制:\(256 MB\)

题解

类似博弈论,所以可以直接写 dfs 求解。

具体地,我们令 \(f_{l, r, k}\) 表示两个人分别到了 \(l, r\),且上一个人走到了 \(k\),会得到的结果。转移方程与博弈论模型类似,是 \(\max\) 里面套两个 \(\min\)

这样做,转移是 \(\mathcal{O}(1)\) 的,但状态是 \(\mathcal{O}(n^3)\) 的,需要优化。

注意到 \(k\) 不会很大,因为很快就取完了。具体地,需要满足 \(\frac{k(k + 1)}{2} \leq n\),因此 \(k\) 这一维开到 \(90\) 即可。

另一方面,两个人轮流取数,所以差不会太大。由于每次取 \(k\)\(k + 1\),所以一个回合最多差 \(1\),即总共不会差超过 \(\sqrt{n}\)

这样,状态就优化成 \(n \cdot \sqrt{n} \cdot \sqrt{n}\) 了,可以通过。

参考代码(记搜版):

#include<bits/stdc++.h>
//#define int long long
using namespace std;
inline int read(){
	int x = 0, f = 1;
	char ch = getchar();
	while(!isdigit(ch)){
		if(ch == '-') f = -1;
		ch = getchar();
	}
	while(isdigit(ch)){
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x * f;
}
inline void write(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
	return;
}
const int _ = 0;
int n, s[4005], val[4005][90][90][2];
bool vis[4005][90][90][2];
inline int dfs(int l, int r, int k, int p){
	if(vis[l][r - l + 1][k][p]) return val[l][r - l + 1][k][p];
	int res = ~(0^_^0);
	if(p == 0)
		if(r - l + 1 < k)
			res = 0;
		else if(r - l + 1 >= k + 1)
			res = max(dfs(l + k, r, k, 1) + s[l + k - 1] - s[l - 1], dfs(l + k + 1, r, k + 1, 1) + s[l + k] - s[l - 1]);
		else
			res = dfs(l + k, r, k, 1) + s[l + k - 1] - s[l - 1];
	else
		if(r - l + 1 < k)
			res = 0;
		else if(r - l + 1 >= k + 1)
			res = min(dfs(l, r - k, k, 0) - s[r] + s[r - k], dfs(l, r - (k + 1), k + 1, 0) - s[r] + s[r - (k + 1)]);
		else
			res = dfs(l, r - k, k, 0) - s[r] + s[r - k];
	vis[l][r - l + 1][k][p] = true;
	return val[l][r - l + 1][k][p] = res;
}
signed main(){
	freopen("game.in", "r", stdin);
	freopen("game.out", "w", stdout);
	n = read();
	for(int i = 1; i <= n; i++) s[i] = s[i - 1] + read();
	write(dfs(1, n, 1, 0));
	return 0;
}

T2 宝石装置

题目描述

Hmr 发现了一个宝石装置,并对其进行了研究:

这个装置由 \(n\) 颗宝石组成,依次编号为 \(1, 2, 3, \cdots, n\)

这些宝石之间由总计 \(m\) 条路径连接。其中第 \(i\) 条路径可以由 \((u_i, v_i, a_i, b_i, x_i)\) 五个参数组成,代表着,在第 \(u_i\) 个宝石被激活之后,这条路径将会开始运作,在 \(a_ix_i + b_i\) 单位时间后,\(v_i\) 也将会被激活。其中 \(u_i, v_i, a_i, b_i\) 是固定的,\(x_i\) 可以由 Hmr 设置成任意非负实数。

在使用这个宝石装置的过程中,Hmr 可以在 \(0\) 时刻主动使用魔力激活任意一个节点 \(S\),然后宝石装置将会通过路径激活其他的宝石。如果一个宝石在不超过 \(V_i\) 时刻激活了,那么称这个宝石是有效的。

如果有效的宝石数量不小于 \(K\),那么这个装置将会开始工作,并释放出 \(\min\limits_{i = 1}^m x_i\) 的能量。

Hmr 想要知道,这个装置最多可以释放多少能量。

数据范围:\(1 \leq K \leq n \leq 2000, 1 \leq m \leq 10^4, 1 \leq u_i, v_i \leq n, 1 \leq a_i, b_i, V_i \leq 10^9\)。时间限制:\(2000 ms\)

题解

赛时想到了 \(\mathcal{O}(nm\log n\log V)\) 做法,但不知道随机序列的严格前缀最大值期望个数(呜呜。

假设答案是 \(mid\),则所有 \(x\) 都取 \(mid\) 肯定是最优的!

因此,考虑二分答案,然后跑全源 Dijkstra 判断即可。

考虑转换顺序,先枚举起点再二分答案,这样二分的左端点可以取目前最大的答案。

这样可以得到一个优化:如果 \(l\) 都不符合要求,就不用二分了。

注意到:随机序列的严格前缀最大值期望只有 \(\mathcal{O}(\log n)\)

因此我们随机调整枚举起点的顺序即可,这样期望只需要进行 \(\mathcal{O}(\log n)\) 次二分答案。

参考代码:

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
//#define int long long
using namespace std;
inline int read(){
	int x = 0, f = 1;
	char ch = getchar();
	while(!isdigit(ch)){
		if(ch == '-') f = -1;
		ch = getchar();
	}
	while(isdigit(ch)){
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x * f;
}
inline void write(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
	return;
}
struct Edge{
	int v, a, b;
};
int n, m, K, id, V[2005];
vector<Edge> e[2005];
struct Node{
	int u; double dis;
	bool operator > (const Node& a) const {
		return dis > a.dis;
	}
};
double dis[2005];
bool vis[2005];
priority_queue<Node, vector<Node>, greater<Node> > q;
set<int> s;
inline bool judge(int start, double x){
	for(int i = 1; i <= n; i++) dis[i] = 1e18;
	memset(vis, false, sizeof(vis));
	while(!q.empty()) q.pop(); s.clear();
	
	q.push({start, 0}), s.insert(start), dis[start] = 0;
	while(!q.empty()){
		if(s.size() >= K) return true;
		int u = q.top().u;
		q.pop();
		if(vis[u]) continue;
		vis[u] = true;
		for(int i = 0; i < e[u].size(); i++){
			int v = e[u][i].v;
			double w = (double)1.0 * e[u][i].a * x + 1.0 * e[u][i].b;
			if(dis[v] > dis[u] + w){
				dis[v] = dis[u] + w;
				if(dis[v] <= V[v]) s.insert(v);
				if(s.size() >= K) return true;
				q.push({v, dis[v]});
			}
		}
	}
	return s.size() >= K;
}
int p[2005];
signed main(){
	freopen("gem.in", "r", stdin);
	freopen("gem.out", "w", stdout);
	n = read(), m = read(), K = read(), id = read();
	for(int i = 1; i <= n; i++) V[i] = read();
	for(int i = 1; i <= m; i++){
		int u = read(), v = read(), a = read(), b = read();
		e[u].push_back({v, a, b});
	}
	for(int i = 1; i <= n; i++) p[i] = i;
	mt19937 rnd(26145983);
	shuffle(p + 1, p + 1 + n, rnd);
	double ans = -1, eps = 0.000001;
	for(int i = 1; i <= n; i++){
		double l = min(ans + eps, 1e9), r = 1e9, res = -1;
		if(!judge(p[i], l)) continue; // !
		while(l <= r){
			double mid = 1.0 * (l + r) / 2.0;
			if(judge(p[i], mid)){
				res = mid;
				l = mid + eps;
			}else{
				r = mid - eps;
			}
		}
		ans = max(ans, res);
	}
	if(ans < 0){
		puts("-1");
	}else{
		cout << fixed << setprecision(6) << ans << '\n';
	}
	return 0;
}

T3 第二个宝石装置

1

T4 无限队列

题目描述

Mdk 发现了一个可数无穷长的队列,其中队列从左往右数第 \(i\) 个元素初始编号为 \(i\)

Mdk 想要对序列进行 \(d\) 次操作,每一次操作描述如下:

她会同时删掉队列中的 \(n\) 个元素,它们分别是从左往右数的第 \(a_1, a_2, \cdots, a_n\) 个元素,空缺的位置将由右侧的元素来补齐。

Mdk 想要知道最后某几个位置的元素的编号,但是她现在并没有时间,所以她希望你来告诉她。她会给你 \(Q\) 个位置 \(x_i\),希望你能够回答 \(d\) 次操作之后,从左往右数第 \(i\) 个数的编号是多少。

数据范围:\(1 \leq n, Q \leq 5 \times 10^5\)\(1 \leq d, a_i, x_i \leq 10^{12}\)\(a_i\) 两两互不相同。

题解

给出部分分做法。

暴力算部分分吗?

Sol 0

依题意模拟即可。

期望得分:\(20\)

Sol 1

优先想到了这个做法。

赛时还记得打过一场 Div.2(Upd on Day2:CF2169D1),那场的 D1 和这道题类似,当时场切了。

考虑到,对于位置 \(x\),一次操作后相当于其下标减少了 \(a\) 中不超过它的数的个数。而且,无论操作多少次,数字的相对位置不会改变。

因此,对于每一组询问,进行二分答案,然后 \(\mathcal{O}(d)\) 检查即可。

期望得分:\(40\)

参考代码(\(40\) 分):

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x = 0, f = 1;
	char ch = getchar();
	while(!isdigit(ch)){
		if(ch == '-') f = -1;
		ch = getchar();
	}
	while(isdigit(ch)){
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x * f;
}
inline void write(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
	return;
}
int n, a[500005], Q, d;
inline int check(int x){
	for(int i = 1; i <= d; i++){
		x -= upper_bound(a + 1, a + 1 + n, x) - a - 1;
	}
	return x;
}
inline int Solve(int x){
	int l = 1, r = 1e18, ans;
	while(l <= r){
		int mid = (l + r) >> 1ll;
		if(check(mid) >= x){
			ans = mid;
			r = mid - 1;
		}else{
			l = mid + 1;
		}
	}
	return ans;
}
signed main(){
	freopen("queue.in", "r", stdin);
	freopen("queue.out", "w", stdout);
	n = read();
	for(int i = 1; i <= n; i++) a[i] = read();
	sort(a + 1, a + 1 + n);
	Q = read(), d = read();
	while(Q--) write(Solve(read())), putchar('\n');
	return 0;
}

Sol 2

考虑优化 Sol 1,这个 trick 来自 Sol 1 中提到的那场 CF 的 D2(Upd on Day2:CF2169D2)。

实际上,对于一个数 \(x\),假设 \(a\) 中最后一个不超过 \(x\) 的数为 \(y\),则 \(x\) 不超过 \(y\) 的情况下每次减少的量不变。

因此,我们可以通过除法代替多次减法,就获得了根号复杂度的算法。

期望得分:\(70\)

参考代码(\(70\) 分):

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("inline")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fstrict-aliasing")
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int x = 0, f = 1;
	char ch = getchar();
	while(!isdigit(ch)){
		if(ch == '-') f = -1;
		ch = getchar();
	}
	while(isdigit(ch)){
		x = (x << 1) + (x << 3) + (ch ^ 48);
		ch = getchar();
	}
	return x * f;
}
inline void write(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
	return;
}
int n, a[500005], Q, d;
inline int check(int x){
	int dd = d;
	while(dd > 0){
		int pos = upper_bound(a + 1, a + 1 + n, x) - a - 1, y = a[pos];
		if(!pos) break;
		int ci = (x - y) / pos;
		ci = max(min(ci, dd), 1ll);
		x -= pos * ci;
		dd -= ci;
	}
	return x;
}
inline int Solve(int x){
	int l = 1, r = 1e18, ans;
	while(l <= r){
		int mid = (l + r) >> 1ll;
		if(check(mid) >= x){
			ans = mid;
			r = mid - 1;
		}else{
			l = mid + 1;
		}
	}
	return ans;
}
signed main(){
	freopen("queue.in", "r", stdin);
	freopen("queue.out", "w", stdout);
	n = read();
	for(int i = 1; i <= n; i++) a[i] = read();
	sort(a + 1, a + 1 + n);
	Q = read(), d = read();
	while(Q--) write(Solve(read())), putchar('\n');
	return 0;
}

Sol 3

考虑正解。

posted @ 2025-11-25 16:19  zhang_kevin  阅读(2)  评论(0)    收藏  举报