2024 信友队 noip 冲刺 10.5

T1

给定一张 nnn 个点 mmm 条边的无向图,边有边权。规定一个参数 xxx,在经过 (ui,vi)(u_i,v_i)(ui,vi) 这条边时,会使 x←x−wix\gets x-w_ixxwi;而任何时刻都需要保证 x≥0x\ge 0x0。现在需要求出一个 x0x_0x0,使得对于任意一对点 (u,v)(u,v)(u,v),都可以合法的从 uuu 走到 vvv,而每当到达一个点都可以操作一次使得 x←x0x\gets x_0xx0,但是在一次行走中最多操作 kkk 次。求最小的 x0x_0x0

n,k≤100n,k\le 100n,k1000≤wi≤1090\le w_i\le 10^90wi109。保证图联通。

上来就写了一个假做法:求出最小生成树然后直接二分 + O(n2)O(n^2)O(n2) 判定,这显然是错的,反例就是 u,vu,vu,v 之间有一条三条边、边权为 666 的路径和一条两条边、边权为 777 的路径,最小生成树不会保留 777 边权的边,但是显然走后者比走前者优。致敬 XYD 传奇随机数据假做法拿了 90pts90\mathrm{pts}90pts

首先 nnn 很小就可以考虑乱搞,先 O(n3)O(n^3)O(n3) 算一遍全源最短路,然后考虑二分这个 x0x_0x0,此时有一个很精妙的转化:对于 u,vu,vu,v 之间的最短路,若长度 >x0>x_0>x0 那么我们将边权更改为 ∞\infty,否则为 111,这样跑出来后 u,vu,vu,v 之间新最短路的长度就是令 x←x0x\gets x_0xx0 的操作次数。为什么?因为我们不能在边上行走时执行操作,于是对于一段 u→vu\to vuv 的路径,我们一定是将路径在点上划分为若干段,使得每段的边权和 <x<x<x 且尽可能长。若有一段的长度比 x0x_0x0 要大,那么即使 xxx 是满的也走不过去,故设为 ∞\infty;否则我们就将这一段视为划分的一段,消耗一次操作,故边权为 111。再跑一次 Floyd 然后 O(n2)O(n^2)O(n2) check 即可。

namespace STD {
	const ll inf = 1e18;
	ll g[maxn][maxn], f[maxn][maxn];
	int main() {
		scanf("%d %d %d", &n, &m, &k);
		for (int i = 1; i <= n; i ++)
			for (int j = 1; j <= n; j ++)
				g[i][j] = i == j ? 0 : inf;
		for (int i = 1, u, v, w; i <= m; i ++)
			scanf("%d %d %d", &u, &v, &w), g[u][v] = g[v][u] = min(g[u][v], 1ll * w);
		for (int _ = 1; _ <= n; _ ++)
			for (int i = 1; i <= n; i ++)
				for (int j = 1; j <= n; j ++)
					g[i][j] = min(g[i][j], g[i][_] + g[_][j]);
		auto check = [&](ll x) { // 新语法
			for (int i = 1; i <= n; i ++)
				for (int j = 1; j <= n; j ++)
					f[i][j] = i == j ? 0 : g[i][j] <= x ? 1 : inf;
			for (int _ = 1; _ <= n; _ ++)
				for (int i = 1; i <= n; i ++)
					for (int j = 1; j <= n; j ++)
						f[i][j] = min(f[i][j], f[i][_] + f[_][j]);
			bool ok = 1;
			for (int i = 1; i <= n; i ++)
				for (int j = 1; j <= n; j ++)
					ok &= (f[i][j] <= k);
			return ok;
		}; ll ans = 1e18;
		for (ll L = 0, R = 1e18; L <= R; ) {
			ll mid = (L + R) >> 1;
			if (check(mid)) ans = mid, R = mid - 1;
			else L = mid + 1;
		} printf("%lld\n", ans); 
		return 0;
	}
}

T2

有一个 n×mn\times mn×m 的棋盘和一个棋子,每个格子有非负权值,Alice 和 Bob 进行博弈:

  • 首先,Alice 在第 111 行中选择一个格子放下棋子,然后获得那个格子的权值;
  • 然后,Bob 选择一个不在最后一行的格子,设为 (i,j)(i,j)(i,j),在 (i,j)(i,j)(i,j)(i+1,j)(i+1,j)(i+1,j) 之间放一堵墙,使得放下之后棋子无法从 (i,j)(i,j)(i,j) 直接走到 (i+1,j)(i+1,j)(i+1,j),也可以选择不放置墙,放完墙后需保证存在至少一条去最后一行的路径;
  • 接着,Alice 选择 444 个可以走的存在的相邻格子中的一个,将棋子挪过去,获得那个格子的权值;
  • 重复操作直到棋子到达最后一行,定义获得的分数为途径所有格子的权值之和,若多次途径同一格则多次计算权值

Alice 的目标是最小化分数,Bob 的目标是最大化分数,求最优操作下最终获得的分数。

1≤n,m≤1001\le n,m\le 1001n,m100

一开始读错题以为横竖都能放墙,硬控半小时,然后认为除了第一行和最后一行其余格子都会拿且仅拿一次,又硬控一小时。最后得到的策略是:Alice 若往左走,那么 Bob 就顺着她走直到碰到边缘,然后再让她飘回到另一端下去到下一行,当然还有直接放 Alice 下去的选择。做个 dp 即可,dp 过程中转移是取 max⁡\maxmax 还是 min⁡\minmin 看是谁在操作。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
const int maxn = 105;
int a[maxn][maxn], g[maxn][maxn], f[maxn][maxn], n, m;
const int inf = 1e9;
int main() {
	open(board);
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m; j ++)
			scanf("%d", &a[i][j]);
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m; j ++)
			g[i][j] = a[i][j] + g[i][j - 1];
	auto getsum = [&](int id, int L, int R) { return g[id][R] - g[id][L - 1]; };
	for (int i = 1; i <= m; i ++)
		f[n][i] = a[n][i];
	for (int i = n - 1; i > 0; i --)
		for (int j = 1; j <= m; j ++) {
			// (i, j) -> (i + 1, k)
			int c1 = 0, c2 = 0, c3 = 0; // c1: -> c2: || c3: <- 
			//                                        \/
			for (int k = 1; k < j; k ++) 
				c1 = max(c1, f[i + 1][k] + getsum(i, j, m) + getsum(i, k, m - 1));
			c2 = a[i][j] + f[i + 1][j];
			for (int k = j + 1; k <= m; k ++)
				c3 = max(c3, f[i + 1][k] + getsum(i, 1, j) + getsum(i, 2, k));
			// cout << i << ' ' << j << ' ' << c1 << ' ' << c2 << ' ' << c3 << '\n';
			f[i][j] = max(c2, min(j == 1 ? inf : c1, j == m ? inf : c3));
		}
	int ans = inf;
	for (int i = 1; i <= m; i ++)
		ans = min(ans, f[1][i]);
	printf("%d\n", ans);
}

然而虽然 AC 但是策略是错的,每次不一定要折返到边缘。正解是考虑在每一行做区间 dp,令 f(i,l,r,0/1)f(i,l,r,0/1)f(i,l,r,0/1) 表示第 iii 行已经在 [l,r][l,r][l,r] 上放了墙,此时棋子在 l−1/r+1l-1/r+1l1/r+1。转移很好做。致敬传奇 XYD 数据写假两题获 190pts190\mathrm{pts}190pts

T3

nnn 位入侵者,当你遇到第 iii 个入侵者时,如果你此前受到的总伤害不大于 HiH_iHi,你将会战胜这个入侵者并受到 PiP_iPi 点伤害,否则你会战败。战败后你会受到 PiP_iPi 点伤害并仍可以和其他入侵者战斗。

你可以以任意顺序和入侵者战斗,并且一开始没有受到任何伤害。求最多能战胜多少个入侵者。

n≤105n\le 10^5n1050≤Hi≤10150\le H_i\le 10^{15}0Hi10150≤Pi≤1090\le P_i\le 10^90Pi109

赛时糊了一档 O(n!)O(n!)O(n!) 暴力分和乱搞获 40pts40\mathrm{pts}40pts。正解是反悔贪心。容易证明,如果 Hi+Pi<Hj+PjH_i+P_i<H_j+P_jHi+Pi<Hj+Pj,那么如果 jjj 选之后 iii 还能选,那么交换它们的选择顺序仍然合法。所以将所有入侵者按 Hi+PiH_i+P_iHi+Pi 排序,答案是其中一个子序列。

考虑反悔堆贪心。维护一个堆表示当前战胜的敌人,从前往后时刻保证当前堆是最大战胜数量且受到伤害最小的。如果当前的入侵者 iii 无法战胜,那么退而求其次,我们尝试弹出一个元素使受到伤害减少。一个显然的想法是弹出 PjP_jPj 最大的元素 jjj。不妨 i≠ji\ne ji=j(否则等价于舍弃当前元素),接下来证明这个策略仍然保证是最优解。首先,由于弹出的是最大值,所以减少的伤害必然不小于上一次受到的伤害,即当前总伤害 P∗≤HiP^∗≤H^iPHi,故方案合法。其次,如果存在更优的方案修改了原方案,则考虑被修改元素中最靠前的序号 xxx 和被修改的元素中最靠前的序号 yyy,则 xxx 一定会先于 yyy 被弹出,故原方案不可能是经过上述策略的方案。

复杂度 O(nlog⁡n)O(n\log n)O(nlogn)

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
struct Invader {
	ll h; int p;
	bool operator<(const Invader &oth) const {
		return h + p < oth.h + oth.p;
	}
} a[maxn];
int n; priority_queue<int> q;
int main() {
	open(invader);
	scanf("%d", &n);
	for (int i = 1; i <= n; i ++)
		scanf("%lld %d", &a[i].h, &a[i].p);
	sort(a + 1, a + n + 1); ll sum = 0;
	for (int i = 1; i <= n; i ++) {
		if (sum <= a[i].h)
			sum += a[i].p, q.push(a[i].p);
		else if (q.top() > a[i].p)
			sum += a[i].p - q.top(), q.pop(), q.push(a[i].p);
	} printf("%d\n", q.size());
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值