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\) 的补完

浙公网安备 33010602011771号