牛客暑假多校 2023 第六场
写在前面
比赛地址:https://ac.nowcoder.com/acm/contest/57360。
哈基米牛魔酬宾,哈比下,哈比下,奥利安费,阿米诺斯!
以下按照个人向难度排序。
G
\(a - b\) 相当于辗转相减,\(\gcd(|a|, |b|)\) 和直接 \(\gcd\) 没什么区别。
于是当 \(z= 0\) 时,\(x,y\) 中一者为 0 则 YES,否则 NO;当 \(z \not= 0\) 时判断 \(z\) 是否是 \(\gcd(x, y)\) 的倍数。
Code by Nebulyu:
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
void solve(){
ll x,y,z;cin>>x>>y>>z;
ll g=gcd(x,y);
if(z==0){
if(x==0||y==0)cout<<"YES\n";
else cout<<"NO\n";
return ;
}
if(z%g)cout<<"NO\n";
else cout<<"YES\n";
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T;cin>>T;while(T--)solve();
return 0;
}
E
鉴于偶数加偶数还是偶数,显然如果一段区间可以被分成 \(k\) 份偶数区间,那么它最多可以被分成不小于 \(k\) 份偶数区间,于是仅需考虑每个区间最多可以分成多少个偶数部分。
赛时想了个恼弹线段树,先将询问按右端点排序,然后考虑枚举右端点,当前一段的和为偶数则进行分割,仅需查询区间被分割的数量即可。这个过程可以用线段树维护,如果这个位置为奇数则在线段树该位置改为 0 并将之前位置全部取反,如果为偶数则将线段树该位置改为 1。枚举过程中查询区间和即可,总时间复杂度 \(O((n + q)\log n)\)。
实际上根本不需要线段树,维护一个数量的前缀和和奇偶性的前缀和即可,总时间复杂度 \(O(n + q)\)。
//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, q;
struct AdmireVega {
int l, r, k, id;
} ayabe[kN];
bool a[kN], ans[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
bool cmp(AdmireVega fir_, AdmireVega sec_) {
return fir_.r < sec_.r;
}
namespace Seg {
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
const int kNode = kN << 2;
int sum[kNode], tag[kNode];
void Pushup(int now_) {
sum[now_] = sum[ls] + sum[rs];
}
void Pushdown(int now_, int L_, int R_) {
sum[ls] = (mid - L_ + 1) - sum[ls];
sum[rs] = (R_ - mid) - sum[rs];
tag[ls] ^= 1, tag[rs] ^= 1;
tag[now_] = 0;
}
void Insert(int now_, int L_, int R_, int pos_, int val_) {
if (L_ == R_) {
sum[now_] = val_;
return ;
}
if (tag[now_]) Pushdown(now_, L_, R_);
if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
else Insert(rs, mid + 1, R_, pos_, val_);
Pushup(now_);
}
void Reverse(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ && R_ <= r_) {
sum[now_] = (R_ - L_ + 1) - sum[now_];
tag[now_] ^= 1;
return ;
}
if (tag[now_]) Pushdown(now_, L_, R_);
if (l_ <= mid) Reverse(ls, L_, mid, l_, r_);
if (r_ > mid) Reverse(rs, mid + 1, R_, l_, r_);
Pushup(now_);
}
int QuerySum(int now_, int L_, int R_, int l_, int r_) {
if (l_ <= L_ && R_ <= r_) return sum[now_];
if (tag[now_]) Pushdown(now_, L_, R_);
int ret = 0;
if (l_ <= mid) ret += QuerySum(ls, L_, mid, l_, r_);
if (r_ > mid) ret += QuerySum(rs, mid + 1, R_, l_, r_);
return ret;
}
bool QueryPos(int now_, int L_, int R_, int pos_) {
if (L_ == R_) return sum[now_];
if (tag[now_]) Pushdown(now_, L_, R_);
if (pos_ <= mid) return QueryPos(ls, L_, mid, pos_);
return QueryPos(rs, mid + 1, R_, pos_);
}
#undef ls
#undef rs
#undef mid
}
void Init() {
n = read(), q = read();
for (int i = 1; i <= n; ++ i) {
LL nowa; scanf("%lld", &nowa);
a[i] = (nowa % 2ll == 1ll);
}
for (int i = 1; i <= q; ++ i) {
int l_ = read(), r_ = read(), k_ = read();
ayabe[i] = (AdmireVega) {l_, r_, k_, i};
}
std::sort(ayabe + 1, ayabe + q + 1, cmp);
}
void Solve() {
for (int nowr = 1, nowq = 1; nowr <= n; ++ nowr) {
if (nowr > 1 && a[nowr] == 1) Seg::Reverse(1, 1, n, 1, nowr - 1);
Seg::Insert(1, 1, n, nowr, (a[nowr] == 0));
while (nowq <= q && ayabe[nowq].r == nowr) {
int ret1 = Seg::QuerySum(1, 1, n, ayabe[nowq].l, nowr);
int ret2 = Seg::QueryPos(1, 1, n, ayabe[nowq].l);
ans[ayabe[nowq].id] = ret2 && (ret1 >= ayabe[nowq].k);
++ nowq;
}
}
for (int i = 1; i <= q; ++ i) printf("%s\n", ans[i] ? "YES" : "NO");
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int T = read();
while (T --) {
Init();
Solve();
}
return 0;
}
C
《我推的式子》
套路地转化为求因子 2 和 5 数量的较小值,显然因子 5 的数量更小,仅需关注如何求因子 5 个数即可。由于 5 的幂的贡献递增,手玩下发现答案即 \(n!!\) 中 5 的倍数的个数,加 25 的倍数的个数,加 625 的倍数的个数……
上面这东西可以分奇偶讨论,枚举 5 的幂次后转化为求等差数列和,详见代码。复杂度 \(O(\log n)\) 级别。
会爆 int128,赛时写了个 py。
py 里的 /
是实数除法,//
是整除,铭记!
def calc(x_):
return x_ * (x_ + 1) // 2
n = int(input())
sum5 = 0
d = 5
for i in range(0, 28):
if d > n:
break
cnt = n // d
if cnt > 0:
odd = cnt // 2
even = (cnt - 1) // 2
sum5 = sum5 + (d + 1) // 2 * calc(odd)
sum5 = sum5 + d // 2 * calc(even)
if cnt % 2 == 1:
sum5 = sum5 + (n - (d * cnt - 1) + 1) // 2 * ((cnt + 1) // 2)
else:
sum5 = sum5 + (n - (d * cnt - 1)) // 2 * (cnt // 2)
d = d * 5
d = 10
for i in range(0, 28):
if d > n:
break
cnt = n // d
if cnt > 0:
sum5 = sum5 + d // 2 * calc(cnt - 1)
sum5 = sum5 + (n - (d * cnt - 1) + 1) // 2 * cnt
d = d * 5
print(int(sum5))
B
先排个序。
显然若两个集合可以互相转化,那么它们大小相同且一定是第 \(k\) 大转化为第 \(k\) 大。\(a_i\) 转化为 \(b_j\) 的操作次数显然为 \(|a_i - b_j|\),于是考虑 \(O(n^2)\) 地枚举被转化的数对,并考虑包含这些数对的集合的贡献。
设 \(f_{i, j}\) 表示从 \(a_1\sim a_{i}\) 和 \(b_1\sim b_j\) 中选择相同个数的数的方案数,\(g_{i, j}\) 表示从 \(a_i\sim a_{n}\) 和 \(b_j\sim b_n\) 中选择相同个数的数的方案数,显然 \((a_i, b_j)\) 对答案的贡献为:
如何求 \(f,g\) 呢?考虑组合意义,不妨令 \(i\le j\),则有:
根据简单的组合数学推导一下:
发现这东西是个经典的范德蒙德卷积的形式,其组合意义相当于先在前 \(i\) 个里面选择 \(i - k\) 个,再在后 \(j\) 个物品中选择 \(k\) 个,等价于直接从 \(i + j\) 个物品中选择 \(i\) 个,则有:
同理 \(g\) 也可以轻松处理了,预处理后直接枚举 \((a_i, b_j)\) 并计算贡献即可。总复杂度 \(O(n^2)\) 级别。
Code by Nebulyu:
#include<bits/stdc++.h>
#define ffor(i,a,b) for(int i=a;i<=b;++i)
#define rfor(i,a,b) for(int i=a;i>=b;--i)
using namespace std;
using ll=long long;
const ll P=998244353;
const int LIM=1e5;
const int M=LIM+5;
const int N=2e3+5;
ll fac[M],ifac[M];
ll C(ll n,ll r){return fac[n]*ifac[r]%P*ifac[n-r]%P;}
ll fsp(ll b,ll k){
ll r=1;for(;k;k>>=1,b=b*b%P)
if(k&1)r=r*b%P;return r;
}
ll rec[N][N];
void init(){
fac[0]=ifac[0]=1;
ffor(i,1,LIM)fac[i]=fac[i-1]*i%P;
ifac[LIM]=fsp(fac[LIM],P-2);
rfor(i,LIM-1,1)ifac[i]=ifac[i+1]*(i+1)%P;
// ffor(i,0,N-1)rec[i][0]=rec[0][i]=1;
ffor(i,0,N-1)ffor(j,0,N-1)rec[i][j]=C(i+j,min(i,j));
}
int n;
ll f1[N],f2[N];
signed main(){
init();
cin>>n;
ffor(i,1,n)cin>>f1[i];
ffor(i,1,n)cin>>f2[i];
sort(f1+1,f1+1+n);
sort(f2+1,f2+1+n);
ll ans=0;
ffor(i,1,n)ffor(j,1,n){
ll l1=i-1,l2=j-1,r1=n-i,r2=n-j;
ans+=rec[l1][l2]*rec[r1][r2]%P*abs(f1[i]-f2[j])%P;
ans%=P;
}
cout<<ans;
return 0;
}
A
场上想到 Kruscal 重构树了但是光想着预处理出两点间的最长边怎么网络流了思路歪了呃呃呃呃
两点间的贡献是路径上的最长边这个性质太典了,考虑 Kruscal 重构树,将原图节点变为重构树中的叶节点,两点间的最长边贡献变为两点 lca 的点权值的贡献,枚举边时将两个子树合并,两子树代表的点集之间的最长边即为枚举到的边的边权,仅需考虑两个子树中分别有多少黑白点即可。
于是可以在 Kruscal 重构树上得到一个很显然的树形 DP,设 \(f_{u, i}\) 表示在 \(u\) 的子树中一共有 \(i\) 个点为黑点时这棵树代表的点集对答案的贡献。如果节点 \(u\) 初始时为白点则初始化 \(f_{u, 0} = 0, f_{u, 1} = -\operatorname{cost}_u\),否则初始化 \(f_{u, 0} = -\operatorname{cost}_u, f_{u, 1} = 0\)。在构建 Kruscal 重构树枚举到边 \((u, v, w)\) 合并 \(u, v\) 所在点集 \(r_u, r_v\) 得到点集 \(r\) 的同时进行转移,考虑从 \(r_u, r_v\) 中分别选择多少个黑点,有:
设最终得到的点集为 \(rt\),答案即:
这样实现起来比较符合重构树的思路也比较直观,但是时间复杂度上限是 \(O(n^3)\) 级别,跑不过去。发现新建节点 \(rt\) 表示合并后的点集是没有必要的,可以在维护并查集时直接按秩合并到原节点上。于是修改定义 \(f_{u, i}\) 表示以 \(u\) 为根的点集中一共有 \(i\) 个点为黑点时这棵树代表的点集对答案的贡献,其余部分均不变。
并查集合并时使用按秩合并时,总时间度复杂度变为 \(O(n^2)\) 级别,甚至只跑了 111ms。
//
/*
By:Luckyblock
*/
#include <cmath>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#define LL long long
const int kN = 3e3 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, rt, a[kN];
int fa[kN], sz[kN];
struct Edge {
int u, v, w;
} e[kN];
LL f[kN][kN], temp[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
bool cmp(Edge fir_, Edge sec_) {
return fir_.w < sec_.w;
}
int Find(int x_) {
return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
}
void Merge(int x_, int y_, LL w_) {
int fax = Find(x_), fay = Find(y_);
int faxy = sz[fax] > sz[fay] ? fax : fay;
for (int i = 0; i <= sz[fax] + sz[fay]; ++ i) {
temp[i] = -kInf;
for (int j = std::max(0, i - sz[fay]); j <= std::min(i, sz[fax]); ++ j) {
int k = i - j;
LL szx0 = sz[fax] - j, szy0 = sz[fay] - k;
temp[i] = std::max(temp[i], f[fax][j] + f[fay][k]
+ w_ * szx0 * k
+ w_ * j * szy0);
}
}
fa[fax] = fa[fay] = faxy;
sz[faxy] = sz[fax] + sz[fay];
for (int i = 0; i <= n; ++ i ) f[faxy][i] = temp[i];
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
n = read();
for (int i = 1; i <= n; ++ i) a[i] = read();
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j <= n; ++ j) {
f[i][j] = -kInf;
}
}
for (int i = 1; i <= n; ++ i) {
int cost = read();
if (a[i]) f[i][1] = 0, f[i][0] = -cost;
else f[i][1] = -cost, f[i][0] = 0;
}
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
e[i] = (Edge) {u_, v_, w_};
}
std::sort(e + 1, e + n, cmp);
for (int i = 1; i <= n; ++ i) fa[i] = i, sz[i] = 1;
for (int i = 1; i < n; ++ i) {
int u_ = e[i].u, v_ = e[i].v, w_ = e[i].w;
Merge(u_, v_, 1ll * w_);
}
LL ans = -kInf;
rt = Find(1);
for (int i = 0; i <= n; ++ i) ans = std::max(ans, f[rt][i]);
printf("%lld\n", ans);
return 0;
}
写在最后
学到了什么:
- 别急着预处理,可能丢失重要信息。
- py 里的
/
是实数除法,//
是整除,铭记!