2025牛客暑期多校训练营2

2025牛客暑期多校训练营2

根据赛时出题人数排序

I

guess

solution

本来想具体证明一下,但官解都不妨设 \(k = 1\) 了。

\(x > 1\)

  • \(x \mod 1 = 0, 1 \mod x = 1, H(x) = 1\)

\(y = 1\)

  • \(y \mod 1 = 0, 1 \mod y = 0, H(y) = 0\)

所以当两个数不相等时,取 \(k = 1\) 必冲突
否则不冲突

code

void func(void)
{
	int a,b;
	cin >> a >> b;
	if(a == 1 || b == 1)	cout << "-1\n";
	else	cout << 1 << '\n';
}

B

solution

设删去 \(x,y(x \ge y), z = x \oplus y\)
如果操作后数值不变小,\(2^x + 2^y \le 2^z\)

除非 \(y = 0\) 否则不可能相等。
事实上只要 \(x < z\),那么等式必然成立,
因为就算 \(z - x = 1\)\(2^z = 2\times 2^x\),而 \(2^y \le 2^x\)

那么只需要判断是否有两个数 \(\oplus\) 变小。
使用贪心的想法,如果高位 \(0 \rightarrow 1\),地位全变成 \(0\) 数值也会变大。
那么只要 \(y\) 最高位 \(1\) 对应到 \(x\) 该位是 \(0\),数值就会变大。

那么只需要把答案非递减排序,每次判断 \(a_i\) 最高位是否出现过 \(1\)

  • 如果出现过,则不保证完美
  • 没有出现,则把这个数的每一位 \(1\) 记录
  • 然后执行下一位

事实上,最多只能处理 \(63\) 个数,\(n > 63\) 可以直接判断

code

void func(void)
{
	int n;
	cin >> n;
	vector<int> a(n+1), cnt(64);
	for(int i=1;i<=n;++i)	cin >> a[i];
	sort(a.begin()+1,a.end(),greater<int>());
	for(int i=1;i<=n;++i)
	{
		bool op = false;
		for(int j=63;j>=0;--j)
		{
			if((a[i]>>j)&1)
			{
				if(!op)
				{
					op = true;
					if(cnt[j])
					{
						cout << "NO\n";
						return;
					}
				}
				cnt[j] ++;
			}
		}
	}
	cout << "YES\n";
}

A

dp低手,开始用线性写错了,还是得用贡献法。

solution

当遇到 \(01\) 的间隙时,视作过了一天。

\(10\) 也行,这样就需要枚举 \(1 \rightarrow n+1\),而非 \(0 \rightarrow n\)

那么遇到 \(0, 1; -1, 1; 0, -1; -1, -1\) 这四种情况可以产生贡献

这一段的贡献取决于有多少可以自由变化的数字。

那么我们枚举间隙,然后计算这两个数之外有多少自由的数字,加入总贡献即可。

code

void func(void)
{
	int n,ans = 0, cnt = 0;	cin >> n;
	vector<int> a(n+1);
	for(int i=1;i<=n;++i)	cin >> a[i];
	for(int i=1;i<=n;++i)	cnt += a[i] == -1;
	for(int i=1;i<=n;++i)
	{
		if(a[i-1] == 1 || a[i] == 0)	continue;
		int z = (a[i] == -1) + (a[i-1] == -1);
		ans = (ans + _2[cnt-z])%P;// _2 为预处理的 2^x
	}
	cout << ans << '\n';
}

F

solution

一次点火操作,只可能在某一个着火点的左右两侧点火,以实现效果最好。

对于一个间隙,设长度为 \(L\),可以分为三种情况

  • \(L \le t_0+1\),无论是否在其间手动点燃,都会完全烧毁。
  • \(t_0+1 < L \le 2t_0+1\)
    • 不抢救将完全烧毁。
    • 点燃其中一点的右/左边第一个位置,可以抢救 \(L-t_0-1\)
  • \(L > 2t_0 + 1\)
    • 不抢救将烧毁 \(2t_0\)
    • 点燃可以抢救 \(t_0-1\)

最后计算在不抢救时不烧毁多少,然后加上可以抢救的最大值即可。

code

void func(void)
{
	int n,t,ans = 0,mx = 0;
	cin >> n >> t;
	string st;	cin >> st;
	vector<int> a;
	for(int i=0;i<n;++i)
	{
		if(st[i] == '1')	a.push_back(i+1);
		else	ans ++;
	}
	a.push_back(a[0]+n);
	for(int i=1;i<a.size();++i)
	{
		int L = a[i] - a[i-1] - 1;
		ans -= min(L,t*2);
		if(L >= t+1)	mx = max(mx,min(L-t-1,t-1));
	}
	cout << ans + mx << '\n';
}

L

solution

就算初始不禁止两位居民,也可能无法配对所有情侣。

因为每个点只有一条入边和一条出边,所以最终会形成若干个有向环。

我们先分析不禁止的情况下,这些环的贡献:

  • 如果一个环为偶数,可以提供两种可能
    • \((1,2),(3,4),\ldots,(n-1,n)\)
    • \((2,3),(4,5),\ldots,(n,1)\)
  • 如果一个环为奇数,将不可能配对

然后我们分析禁止后的贡献:

  • 偶数只能禁止 \(0\)\(2\) 个居民

    因为禁止 \(1\) 个居民后变为奇环

    • 禁止 \(2\) 个的贡献为 \(2 \times (\frac{n}{2})^2\)
    • 不禁止的贡献则任然为 \(2\)
  • 奇数只能禁止一个居民,贡献为 \(n\)

注意: 如果环内只有 \(2\) 个元素,那么不删时贡献为 \(1\), 删掉后贡献为 \(0\)

那么我们可以判定,只有 \(0\) 个奇环或者 \(2\) 个奇环时可以成功配对,

  • \(0\) 个奇环,则计算禁止各个偶环居民的结果的和。
  • \(2\) 个奇环,则计算这禁止这两个奇环的结果的和。

而且我们可以发现,我们把环视作联通图也不会影响结果的求解,因为我们只关注环内元素的个数,而不关注其他信息。所以这里直接使用并查集求各个联通块的大小。然后根据上面的分析分类讨论。

code

代码赛时写的,可能有点抽象。

int n;
int a[N], rt[N], cnt[N];

int inv(void)// 表示为 $/2$ 操作
{
	return (P+1)/2;
}

int find(int x)
{
	return rt[x] = (rt[x] == x ? x : find(rt[x]));
}

void merge(int x,int y)
{
	rt[find(x)] = find(y);
}

void func(void)
{
	int n;	cin >> n;
	for(int i=1;i<=n;++i)
	{
		cin >> a[i];
		cnt[i] = 0;
		rt[i] = i;
	}
	for(int i=1;i<=n;++i)	merge(i,a[i]);
	for(int i=1;i<=n;++i)	cnt[find(i)] ++;
	vector<int> cir;
	for(int i=1;i<=n;++i)
	{
		if(cnt[i])	cir.push_back(cnt[i]);
	}
	int num1 = 0, ans = 0;
	for(auto &i : cir)	num1 += (i&1);
	if(num1 == 0)
	{
		int sum = 1;
		for(auto &i : cir)	sum = (sum*(i == 2 ? 1 : 2))%P;
		for(auto &i : cir)	ans = (sum*(i == 2 ? 1 : inv())%P*i%P*inv()%P*i%P*inv()%P + ans)%P;
	}
	else if(num1 == 2)
	{
		ans = 1;
		for(auto &i : cir)
		{
			if(i&1)	ans = ans*i%P;
			else	ans = ans*(i == 2 ? 1 : 2)%P;
		}
	}
	cout << ans << '\n';
}

H

最短路+凸包

补的时候有考虑能否用线段树维护曲线,毕竟区间修改就是一个等差数列,但考虑到没写过凸包的题目,顺带学下凸包了。

图论低手,可能讲的很差

solution

假设 \(1 \rightarrow n\) 是一条链,那么我们肯定是给 \(w_{max}\) 的路径升级。

推广到其他图同理,我们必然会给最终路径上 \(w\) 最大的路径升级。

在考虑多个询问前,我们考虑只有一个询问下怎么确定这条路径?
可以对 \(1\) 正向 和 \(n\) 反向跑两次 \(dijkstra\),然后枚举路径,然后直接取最小值。

但对 \(q\) 个询问,复杂度变为 \(mq \le 9 \times 10^{10}\) ,完全无法接受。

因为我们确定每次询问只会对一条路径操作,那么我们观察该路径:

升级该路径后耗时:\(dis1_u + dis2_v + t_{u,v} - k \times w_{u,v}\)

这些路径都是关于 \(k\) 的一次函数(且斜率均为负),最后答案实际是这些函数的下凸包

随意画出几条就很好理解了。

然后我们二分 \(k\) 在哪条直线上(每条直线只能管辖一部分连续定义域)即可。

还有一些细节:

  • 对于斜率相同的直线,实际有效果的是截距最小的直线。
  • 若是用交叉相乘判断点的大小关系,可能爆 longlong,需要开 __int128

code

struct Node
{
	int y,t,w;
};

int n,m,top;
vector<Node> v[2][N];
int d[2][N];
PII stk[N];

void dijkstra(int st,int op)
{
	for(int i=1;i<=n;++i)	d[op][i] = inf;
	priority_queue<PII,vector<PII>,greater<PII>> pq;
	d[op][st] = 0;
	bitset<N> vis;
	pq.push({0,st});
	while(pq.size())
	{
		int x = pq.top().Y;	pq.pop();
		if(vis[x])	continue;
		vis[x] = true;
		for(auto &[y,t,w] : v[op][x])
		{
			if(d[op][y] > d[op][x] + t)
			{
				d[op][y] = d[op][x] + t;
				pq.push({d[op][y],y});
			}
		}
	}
}

bool check(PII a,PII b,PII c)
{
	return (i128)(c.X-b.X)*(a.Y-b.Y) <= (i128)(b.X-a.X)*(b.Y-c.Y);
}

void func(void)
{
	cin >> n >> m;
	top = 0;
	for(int i=1;i<=n;++i)
	{
		v[0][i].clear();
		v[1][i].clear();
	}
	for(int i=1;i<=m;++i)
	{
		int x,y,t,w;	cin >> x >> y >> t >> w;
		v[0][x].push_back({y,t,w});
		v[1][y].push_back({x,t,w});
	}
	dijkstra(1,0), dijkstra(n,1);
	vector<PII> tmp(m),edge;
	int idx = 0;
	for(int i=1;i<=n;++i)
	{
		for(auto &[y,t,w] : v[0][i])	tmp[idx ++] = {-w,d[0][i]+d[1][y]+t};// k,b
	}
	sort(tmp.begin(),tmp.end(),[&](PII a,PII b)
	{
		return a.X == b.X ? a.Y < b.Y : a.X < b.X;
	});
	for(auto &i : tmp)
	{
		if((!edge.size() || i.X != edge.back().X) && i.Y < inf)	edge.push_back(i);
	}
	for(int i=0;i<edge.size();++i)
	{
		while(top >= 2 && check(stk[top-1],stk[top],edge[i]))	top --;
		stk[++ top] = edge[i];
	}
	stk[0] = {0,stk[1].Y};
	int q;	cin >> q;
	while(q --)
	{
		int x;	cin >> x;
		int l = 1,r = top-1, tp = top;
		while(l <= r)
		{
			int mid = (l+r) >> 1;
			if((i128)stk[mid].Y-stk[mid+1].Y >= (i128)x*(stk[mid+1].X-stk[mid].X))	l = mid+1;
			else	r = mid-1, tp = mid;
		}
		int ans = stk[tp].X*x + stk[tp].Y;
		cout << ans << '\n';
	}
}

other

计算几何暂时略过,通过 \(< 100\) 的等 \(\ge 100\) 的补完

posted @ 2025-08-21 20:24  zerocloud01  阅读(16)  评论(0)    收藏  举报