2022 ICPC - 昆明站 —— F (思维,二分答案),D(构造,二进制)

F - Find the Maximum
题意:

给定一棵 n 个节点的树,每个节点有点权 w i w_i wi
选择一条点数至少为 2 的链,链上的所有点构成集合 V。
∑ u ∈ V ( − x 2 + b u x ) ∣ V ∣ \frac{\sum_{u \in V}\left(-x^{2}+b_{u} x\right)}{|V|} VuV(x2+bux) 的最大值。

( 2 ≤ n ≤ 1 0 5 , − 1 0 5 ≤ b i ≤ 1 0 5 ) (2\le n \le 10^5,-10^5\le b_i \le 10^5) (2n105,105bi105)

思路:

将所给式子化简后为: − x 2 + b 1 + b 2 + . . . + b ∣ V ∣ ∣ V ∣ x -x^2 + \frac{b_1 + b_2 + ... +b_{|V|} }{|V|}x x2+Vb1+b2+...+bVx
即二元一次方程: − x 2 + k x -x^2 + kx x2+kx,当 x = k 2 x = \frac{k}{2} x=2k 时,有最大值 k 2 4 \frac{k^2}{4} 4k2
所以,要使 k 为正数时尽可能大,或为负数尽可能小

所以题目转化为,选择一条链,使得链上所有点的平均值尽可能大或尽可能小。

先来说正解:链上点的个数为 2 或者为 3 时,平均值能够最大或最小。

如果链上点的个数能为1的话,那么平均值最大肯定挑选一个点权最大的点,平均值最小肯定挑选一个点权最小的点。
但是该题中链上点的个数不能为1,所以只能挑选两个相邻的比较大的点取平均值。
此外,如果两个权值较大的点通过一个权值较小的点相连,那么这三个点取平均值可能会比其中两个相连点取平均值更优。所以还可以挑选三个相邻的点取平均值。
但是,如果四个及以上相邻点取平均值的话,肯定没有取其中两个或者三个相邻点的平均值更优。
所以,只需要考虑相邻的两个或者三个点取平均值即可。

可以遍历每个点的相邻节点,取最大和次大的两个节点。
该节点和最大相邻节点,这两个节点取平均值。
该节点和最大,次大相邻节点,这三个节点取平均值。

实现1:直接遍历相邻节点

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

const int N = 200010, mod = 1e9+7;
int T, n, m;
double a[N];
double ans, w[N];
vector<int> e[N];

signed main(){
	Ios;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	
	for(int i=1;i<n;i++)
	{
		int x, y;cin >> x >> y;
		e[x].pb(y);
		e[y].pb(x);
	}
	
	for(int i=1;i<=n;i++)
	{
		int cnt = 0;
		
		for(auto tx : e[i]) a[++cnt] = w[tx];
		
		sort(a+1, a+cnt+1);
		if(cnt >= 1) ans = max({ans, fabs((a[1]+w[i])/2), fabs(a[cnt]+w[i])/2});
		if(cnt >= 2) ans = max({ans, fabs(a[1]+a[2]+w[i])/3, fabs(a[cnt]+a[cnt-1]+w[i])/3});
	}
	
	printf("%.10f", ans*ans*0.25);
	
	return 0;
}

实现2:dfs

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

const int N = 200010, mod = 1e9+7;
int T, n, m;
double a[N];
double ans, w[N];
vector<int> e[N];

void dfs(int x, int fa)
{
	int max1 = -1e9, max2 = -1e9;
	int min1 = 1e9, min2 = 1e9;
	
	for(auto tx : e[x])
	{
		if(w[tx] >= max1) max2 = max1, max1 = w[tx];
		else if(w[tx] >= max2) max2 = w[tx];
		
		if(w[tx] <= min1) min2 = min1, min1 = w[tx];
		else if(w[tx] <= min2) min2 = w[tx];
		
		ans = max(ans, fabs((w[x]+w[tx])*0.5));
		
		if(tx == fa) continue;
		
		dfs(tx, x);
	}
	
	ans = max(ans, (max1 + max2 + w[x])*1.0/3);
	ans = max(ans, -(min1 + min2 + w[x])*1.0/3);
}

signed main(){
	Ios;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	
	for(int i=1;i<n;i++)
	{
		int x, y;cin >> x >> y;
		e[x].pb(y);
		e[y].pb(x);
	}
	
	dfs(1, 0);
	
	printf("%.10f", ans*ans*0.25);
	
	return 0;
}

思路2:
其实,看到使得平均值最小或者最大,最先想到的是二分答案。二分平均值,看能否更大或者更小。

  • 如果要平均值为正数并且最大,二分平均值mid,将所有点权都减去mid。如果存在一个最大权值链满足其总和大于等于0,说明该链原来平均值是大于等于mid了,能够满足。mid可以继续往右走。
  • 如果要平均值为负数并且最小,二分平均值mid,将所有点权都减去mid。减去一个负数相当于加上一个正数。如果存在一个最小权值链满足其总和小于等于0,说明该链原来平均值是小于等于mid的,能够满足。mid可以继续往左走。
#include<bits/stdc++.h>
using namespace std;

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], w[N];
vector<int> e[N];
int flag;
double f[N], ans, eps = 1e-10;

double dfs(int x, int fa, double mid)
{
	double max1 = 0, max2 = -1e9;
	
	for(auto tx : e[x])
	{
		if(tx == fa) continue;
		double d = dfs(tx, x, mid);
		if(d > max1) max2 = max1, max1 = d;
		else if(d > max2) max2 = d;
	}
	if(max1 + max2 + w[x] - mid >= -eps) flag = 1;
	
	max1 += w[x] - mid;
	return max1;
}

bool check(double mid)
{
	flag = 0;
	dfs(1, 0, mid);
	
	if(flag) return 1;
	return 0;
}

double dfs1(int x, int fa, double mid)
{
	double min1 = 0, min2 = 1e9;
	
	for(auto tx : e[x])
	{
		double d = dfs1(tx, x, mid);
		if(d < min1) min2 = min1, min1 = d;
		else if(d < min2) min2 = d;
	}
	
	if(min1 + min2 + w[x] - mid <= eps) flag = 1;
	
	min1 += w[x] - mid;
	return min1;
}

bool check1(double mid)
{
	flag = 0;
	dfs1(1, 0, mid);
	
	if(flag) return 1;
	return 0;
}

signed main(){
	Ios;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	
	for(int i=1;i<n;i++)
	{
		int x, y;cin>>x>>y;
		e[x].pb(y);
	}
	
	double l = 0, r = 1e5;
	while(r - l > 1e-9)
	{
		double mid = (l+r)/2;
		if(check(mid)) l = mid;
		else r = mid;
	}
	ans = l;
	
	l = -1e5, r = 0;
	while(r - l > 1e-9)
	{
		double mid = (l+r)/2;
		if(check1(mid)) r = mid;
		else l = mid;
	}
	ans = max(ans, -l);
	
	printf("%.10f", ans*ans*0.25);
	
	return 0;
}

但是在这道题中,这种做法被卡精度了。。

这种思路来源于 经典题:最佳牛围栏,为什么该题不能用第一种思路想呢?因为这道题中规定了至少选择 k 个,所以就不能单纯的选一个选两个选三个了。


D - Divisions
题意:

给定一个长度为 n 的数列 S,现在要将其分成两个子串 S1,S2。
从前往后遍历每个位置,每个位置可以加到子串 S1 后面,也可以加到子串 S2 后面。最终子串可以为空。那么一共有 2 n 2^n 2n 种分法。

现在要求分成的两个子串 S1, S2 要满足:

  • S A , 1 ≤ S A , 2 ≤ ⋯ ≤ S A , n A S_{A, 1} \leq S_{A, 2} \leq \cdots \leq S_{A, n A} SA,1SA,2SA,nA
  • S B , 1 ≥ S B , 2 ≥ ⋯ ≥ S B , n B S_{B, 1} \geq S_{B, 2} \geq \cdots \geq S_{B, n B} SB,1SB,2SB,nB

现在给出分配的方案数 k,构造一个数列 S 使其满足分配满足子串的方案数恰好为 k。

找规律:

  • 如果只是一种数字的话,那么能够构造出 2 n 2^n 2n 种方案。
  • 在此串后面加上另外一种数字,加 1 个的话,多出一种方案;2 个又会多出 2 种方案,3 个又会多出 4 种方案…
    那么对于一个新的数字,加 k 个就会多出 2 k − 1 2^k - 1 2k1 种方案。

而对于任意一个数,可以由二进制数拼凑而成,所以也可以由若干个 二进制数-1 组成。

将所有的 二进制数-1 存到数组中,每次找到最大的不超过当前数的数。

注意特判 n=0 和 n=1 的情况,当 n=0 时也可以满足。

Code:
#include<bits/stdc++.h>
using namespace std;

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], ans[N];

signed main(){
	Ios;
	cin>>n;
	
	for(int i=1;i<=32;i++) a[i] = (1ll<<i)-1;
	
	if(n == 0){
		cout << "5\n3 4 2 1 2";
		return 0;
	}
	if(n == 1){
		cout << "6\n1 1 4 5 1 4";
		return 0;
	}
	
	int cnt = 0;
	for(int i=1;;i++)
	{
		if((1ll << i) <= n) cnt = i;
		else break;
	}
	n -= (1ll<<cnt);
	
	int t = 0;
	for(int i=1;i<=cnt;i++) ans[++t] = 1;
	
	int x = 1;
	while(n)
	{
		x ++;
		int l = 1, r = 32;
		while(l < r)
		{
			int mid = l+r+1>>1;
			if(a[mid] <= n) l = mid;
			else r = mid-1;
		}
		n -= a[l];
		for(int i=1;i<=l;i++) ans[++t] = x;
	}
	cout << t << endl;
	for(int i=1;i<=t;i++) cout<<ans[i]<<" ";
	
	return 0;
}

不太好想,平常做的构造题太少了!!
要多刷刷构造题。


在这里总结以下吧。

第二次参加区域赛,又打铁了。
场上上面的两道题都没有开出来,第一题想的是第二种思路,被卡精度了,然后三个人一起看这题,想着思路已经这么清楚明了了,一定要把这题先开出来。然后就一直提交一直试,按着这种思路写了两三份代码。
然后一直到最后都没能调出来。

最后也没心情看构造题D了。一题打铁。。

其实,此前一直没注意到过精度问题,这场被精度卡了两次(签到题因为精度wa了两次。。),长记性了!
回过头来看第一题F,其实也不难想,要使平均值最大,那么选一个点就能最大,但是规定不能只选一个,那就选两个。写几个样例也能发现选三个也行,这样不就想出来了么。。
主要是那个思路已经这么清晰了,没想到会调不出来最后。也没想到转化思路,谁知道是个思维题。。

看来思维还是不行,要多刷思维题和构造题了。

当时第一次看到F题的时候,看到式子这么复杂就没想着去化简,然后等了一段时间队友化简了一下才发现能做。这期间的时间都浪费掉了。。
为什么不化化式子再说呢?还是做的这样的题目少,总结的少,见的少。不敢化简!看着复杂就想跳过!
以后见到式子题要敢化,说不定化简化简就能做了!!

算是又一次吸取经验的宝贵经历吧。

加油,一定能行!!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值