USACO 2022年12月比赛 银组题解

T1. Barn Tree

题解:

本题话说有一个拓扑排序的神奇做法,但是本蒟蒻不太会。

我们先分析一下问题: 可以发现,我们的最优策略总是先从下到上接受下层多余的干草块,再从上往下发送下层所需要的干草块。证明如下:

上传时,相对点权和为正的子树向根节点贡献该子树内多余的点权和,这保证了经过上传操作后,这些子树内的相对点权和为 0。此时,这棵子树内所有点权和为负的子树所需的点权和之总和都集中在根结点处,然后按需下传即可。这意味着点权需求问题在最小的子树内即可得到解决。而最小的子树意味着最少的搬运次数,也就代表着最优策略。

接着,我们就可以想一想怎么实现这道题的代码了。我们考虑先一遍\mathrm{dfs}来找出一个数列f.其中f_x表示当前节点x中的干草捆数量比所有节点平均的干草捆储量多出了多少。特别的,如果x中的干草捆数量小于所有节点中干草捆储量的平均值,则必有f_x<0.

我们再一遍\mathrm{dfs}, 输出方案。先扫出f_x<0的干草捆,再扫出f_x\ge 0的干草捆即可。

#include <bits/stdc++.h>
using namespace std;
#define int long long
int n, sum, aver, ans, a[200005], f[200005]; 
vector<int> G[200005];
void dfs(int cur, int fa) {
	for (auto i : G[cur]) {
		if (i == fa) continue;
		dfs(i, cur);
	}
	f[cur] = a[cur] - aver;
	a[fa] += f[cur];
	ans += f[cur] != 0;
}
void print(int cur, int fa) {
	for (auto i : G[cur]) {
		if (i == fa || f[i] < 0) continue;
		print(i, cur);
	}
	for (auto i : G[cur]) {
		if (i == fa || f[i] >= 0) continue;
		cout << cur << ' ' << i << ' ' << -f[i] << endl;
		print(i, cur); 
	}
	if (f[cur] > 0)
		cout << cur << ' ' << fa << ' ' << f[cur] << endl;
}
signed main() {
    ios::sync_with_stdio(false);
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i], sum += a[i];
	aver = sum / n;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1, 0);
	cout << ans << endl;
	print(1, 0);
	return 0;
}

T2. Circular Barn

题解:

首先我们简化一下问题。假如只有一个牛棚,那该怎么做呢?

我们发现,如果牛棚中牛的数量是4的倍数,那么Farmer Nhoj就会赢;如果牛棚中牛的数量不是4的倍数,那么Farmer John就能获胜。这是因为,Farmer John只要取1头牛,2头牛,或3头牛,把牛的数量变成4的倍数即可获胜。

但是问题来了:由于这里不止一个牛棚,所以求出对于每一个牛棚,是Farmer Nhoj会赢还是Farmer John会赢,还是远远不够的。我们还需要求出:对于每一个牛棚,Farmer John或Farmer Nhoj到底在几轮之后会获胜

分析一些基本的原则:对于Farmer John来说,如果他在第i个牛棚能够轻松获胜,那么他一定会尽可能提早拿光这个牛棚;否则,如果他在第j个牛棚面临着P-Position, 那么他一定会尽可能拖长自己失败的过程。

先来看看Farmer John会获胜的情况:

由于Farmer John必须要把牛的数量变成4的倍数才可以获胜,所以Farmer John必须把牛的数量减去一个和他模4同余的素数(或者是1)。而这些素数我们可以使用埃氏筛处理出来。由于Farmer John想要尽早在这个牛棚中获得胜利,所以他一定会尽可能取走更多的奶牛。我们就可以二分出Farmer John取走奶牛的数量,这里不妨设为x。那么,Farmer John在\lfloor\frac{a_i-x}{4}\rfloor+1轮后会拿光这个牛棚,获得胜利。

再来看看Farmer John必输的情况:

这种情况就没有那么麻烦了,稍加思索可得: Farmer John会在\frac{a_i}{4}+1轮后遭殃。

于是,我们只要找到结束时间最早的牛棚,然后看看谁赢谁输,就完美AC了。

#include<bits/stdc++.h>

using namespace std;
#define int long long
int T, n;
int b[200005];
bool prime[5000111];

struct cownteger {
    int num, id;
    bool winorlose;

    //farmer john能赢 1 否则 0
    bool operator<(const cownteger &_) {
        if (num != _.num) return num < _.num;
        return id < _.id;
    }
} a[200005];

vector<int> pn[4];

int find(int num) {
    int pl = num % 4;
    int l = 0, r = pn[pl].size();
    while (l < r - 1) {
        int mid = (l + r) >> 1;
        if (pn[pl][mid] <= num) l = mid;
        else r = mid;
    }
    return pn[pl][l];
}

void init() {
    for (int i = 1; i <= 5000000; ++i)
        prime[i] = 1;
    int t = sqrt(5000000);
    for (int i = 2; i <= t; ++i)
        if (prime[i] != 0) for (int j = i; j <= 5000000 / i; ++j) prime[i * j] = 0;
    for (int i = 1; i <= 5000000; ++i)
        if (prime[i])
            pn[i % 4].push_back(i);
}

void chmin(cownteger &x, cownteger y) {
    if (y < x) x = y;
}

void solve() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &b[i]), a[i].id = i;
    for (int i = 1; i <= n; ++i) {
        a[i].winorlose = (b[i] % 4 != 0);
        if (b[i] % 4 == 0) a[i].num = b[i] / 4 + 1;
        else {
            a[i].num = (b[i] - find(b[i])) / 4 + 1;
        }
    }
    cownteger final = {INT_MAX, -1, 0};
    for (int i = 1; i <= n; ++i)
        chmin(final, a[i]);
    if (final.winorlose == 1) printf("Farmer John\n");
    else printf("Farmer Nhoj\n");
}

signed main() {
    init();
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}

T3. Range Reconstruction

题解:

我们发现最有用的 r 是 r_{i,i+1},因为它可以告诉我们相邻两个数的差,于是就已经得到了 2^n 的暴力解法。

暴力显然过不去,然而我们又发现一条性质,如果我们知道相邻三个数的极差,又知道了相邻两个数的差,我们是可以断定三个数之间的相对关系的,因为假设两个数的差分别是 a,b ,那么三个数的极差一定是 a+b 或 ∣a−b∣。

所以我们先假定前两个数是第一个数更大,这样可以依次推出后面的数应该是几。

这时发现如果前两个数相同,我们不能知道第三个数,因为不管第三个数更大还是更小三个数的极差都一样,所以我们推广一下做法,找到相邻的三组不一样的数,这样仍然能利用三组数的极差和相邻两组数分别的差算出答案。

#include <bits/stdc++.h>
using namespace std;
int n, lst, ans[305], a[305][305]; 
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++)
		for (int j = i; j <= n; j++)
			cin >> a[i][j];
	ans[1] = 0;
	ans[2] = ans[1] + a[1][2];
	lst = 2;
	for (int i = 3; i <= n; i++) {
		if (a[lst - 1][i] == a[lst - 1][lst] + a[lst][i]) {
			if (ans[lst] < ans[lst - 1]) ans[i] = ans[lst] - a[lst][i];
			else ans[i] = ans[lst] + a[lst][i];
		} else {
			if (ans[lst] < ans[lst - 1]) ans[i] = ans[lst] + a[lst][i];
			else ans[i] = ans[lst] - a[lst][i];
		}
		if (a[i - 1][i] != 0) lst = i;
	}
	int f = 1e9;
	for (int i = 1; i <= n; i++)
		f = min(f, ans[i]);
	for (int i = 1; i <= n; i++)
		cout << ans[i] - f << ' ';
	cout << endl;
	return 0;
}


借鉴

P8902 - 聪明智多星 的博客 - 洛谷博客 (luogu.com.cn)
【题解】P8900 Barn Tree - Lantrol 的博客 - 洛谷博客 (luogu.com.cn)
 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值