The 2022 ICPC Asia Hangzhou Regional Programming Contest

写在前面

比赛地址:https://codeforces.com/gym/104090

以下按个人难度向排序。

最上强度的一集,然而金牌题过了铜牌题没过,唐!

去年杭州似在一道树题上痛失 Au 呃呃,vp 2022 Au 树题过了然而铜牌题没过呃呃

F

签到。

大力模拟。

code by dztle:

复制复制
#include <bits/stdc++.h>
using namespace std;
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;
while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar();
return x*f;
}
#define ull unsigned long long
const int N=100005;
int n,m;
string s;
map<string,int>ma;
int main(){
n=read();
for(int i=1;i<=n;++i){
m=read();
bool ok=0;
for(int j=1;j<=m;++j){
bool fl=0;
cin>>s;
int len=s.length();
for(int k=0;k<len;++k){
if(k+2==len) break;
if(s[k]=='b'&&s[k+1]=='i'&&s[k+2]=='e') fl=1;
}
if(fl==1){
if(ma.count(s)==0){
ok=1;
ma[s]=1;
cout<<s<<'\n';
}
}
}
if(!ok){
cout<<"Time to play Genshin Impact, Teacher Rice!\n";
}
}
return 0;
}
/*
6
1
biebie
1
adwlknafdoaihfawofd
3
ap
ql
biebie
2
pbpbpbpbpbpbpbpb
bbbbbbbbbbie
0
3
abie
bbie
cbie
*/

D

结论。

写了个暴力模拟打表,发现最终数列形态一定为 2:1:1::1

The proof is left as exercise for the readers.

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
const int kN = 1e6 + 10;
double a[kN], sum;
//=============================================================
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int n; std::cin >> n;
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
sum += a[i];
}
sum /= (n + 1);
std::cout << std::fixed << std::setprecision(10) << 2 * sum << " ";
for (int i = 1; i < n; ++ i) std::cout << std::fixed << std::setprecision(10) << sum << " ";
// for (int T = 1; T <= 1000; ++ T) {
// for (int i = 0; i < n; ++ i) {
// a[(i + 1) % n] += a[i] / 2.0;
// a[i] /= 2.0;
// }
// // for (int i = 0; i < n; ++ i) std::cout << std::fixed << std::setprecision(10) << a[i] << " ";
// // std::cout << "\n";
// }
// for (int i = 0; i < n; ++ i) std::cout << std::fixed << std::setprecision(10) << a[i] << " ";
return 0;
}

C

DP。

对于给出的三种取物品的贡献,发现第三种情况至多贡献一次。

fi,j,0/1 表示使用物品 1i,且其中某个物品是/否以第三种情况产生贡献。初始化 f0,0,0=0,转移时考虑当前物品的贡献形式,则有:

pijk, fi,j,0{fi1,j,0fi1,jpi,0+wi,pi

1jk, fi,j,1{fi,j,1max1lmin(j,pi1)fi,jl,0+wi,l

发现这个转移方程是一个显然的背包形式,套路地滚掉第一维按 01 背包实现即可。

特别地,需要考虑无第三种情况的影响。若 pik 则答案为 fpi,0,否则答案为 max(fk,0,fk,1)

总时间复杂度 O(nk) 级别加一个 10 的常数。

code by dztle:

#include <bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;
while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar();
return x*f;
}
const int N=30005;
int n,k,p[N],w[N][15];
int f[N][2];
signed main(){
// freopen("data.in","r",stdin);
// freopen("ans.out","w",stdout);
n=read(),k=read();
int sum=0;
for(int i=1;i<=n;++i){
p[i]=read();
sum+=p[i];
for(int j=1;j<=p[i];++j){
w[i][j]=read();
}
}
// cout<<sum<<endl;
memset(f,-0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;++i){
for(int j=k;j>=1;--j){
if(j>=p[i]){
f[j][0]=max(f[j][0],f[j-p[i]][0]+w[i][p[i]]);
f[j][1]=max(f[j][1],f[j-p[i]][1]+w[i][p[i]]);
}
for(int o=p[i]-1;o>=1;--o){
if(j>=o) f[j][1]=max(f[j][1],f[j-o][0]+w[i][o]);
}
}
// for(int j=1;j<=k;++j){
// cout<<j<<' '<<f[j][0]<<' '<<f[j][1]<<endl;
// }
}
if(sum>=k)cout<<max(f[k][0],f[k][1]);
else cout<<f[sum][0];
return 0;
}
/*
4 20
3 1 1 3
3 1 1 3
3 3 1 1
3 3 1 3
*/

K

字符串,枚举。

读完题看看数据范围感觉像一个很怪的复杂度,发现 O(262) 地询问一次大概是能过的。

考虑字符串比较字典序的本质——两个字符串的字典序仅与它们第一个不同的字符有关。

则当字符优先级调整时,实际上并不需要重新求得所有字符串的字典序:当某字符串为另一字符串的真前缀时字典序恒定更小不受影响,否则仅需重新比较它们两两之间的第一个不同的字符即可。

于是考虑预处理 s 表示满足下列条件的二元组 (i,j) 的数量:

  • 1i<jn
  • sjsi 的真前缀。
  • 实际含义为不受字符优先级影响的逆序对的数量。

预处理数组 fc1,c2 表示满足下列条件的二元组 (i,j) 的数量:

  • 1i<jn
  • si,sj 间不存在前缀关系,且它们第一个不相同的位置 k 满足 c1=sj,k,c2=si,k

上述两者可在顺序枚举字符串,插入 Trie 时顺便求得。具体地:

  • 当插入字符串 sj 时,每次对当前节点 u 按当前枚举到的字符 sj,k 进行转移时,则对于其他转移 trans(u,c)(csj,k) 子树中所有字符串 si,它们第一个不相同的位置即为 k 且满足 c1=sj,k,c2=c,对 fsj,k,c 的贡献为 size(trans(u,c))
  • 当插入结束到达终止状态 u 时,其子树中所有字符串即为所有以 sj 为真前缀的字符串,令 s:=s+size(trans(u,c)) 即可。

预处理后每次询问即可枚举有序的字符二元组 c1,c2(c1<c2) 遍历数组 f,答案即为:

s+c1<c2fc1,c2

总时间复杂度 O(|s|+262q)

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kM = 30;
const int kN = 1e6 + 10;
//=============================================================
int n, q;
std::string s;
LL cnt[kM][kM], sum;
//=============================================================
namespace Trie {
const int kNode = kN << 3;
int nodenum = 1, tr[kNode][30], sz[kNode];
void Insert() {
int now = 1;
for (int i = 0, len = s.length(); i < len; ++ i) {
int ch = s[i] - 'a';
if (!tr[now][ch]) tr[now][ch] = ++ nodenum;
for (int j = 0; j < 26; ++ j) {
if (j == ch) continue;
cnt[ch][j] += sz[tr[now][j]];
}
now = tr[now][ch];
++ sz[now];
}
for (int j = 0; j < 26; ++ j) {
sum += sz[tr[now][j]];
}
}
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> q;
for (int i = 1; i <= n; ++ i) {
std::cin >> s;
Trie::Insert();
}
while (q --) {
std::string t; std::cin >> t;
LL ans = 0;
for (int i = 0; i < 26; ++ i) {
for (int j = i + 1; j < 26; ++ j) {
ans += cnt[t[i] - 'a'][t[j] - 'a'];
}
}
std::cout << ans + sum << "\n";
}
return 0;
}
/*
4 1
a
ab
abc
a
abcdefghijklmnopqrstuvwxyz
*/

A

数论。

前置知识:裴蜀定理扩展。对于 n 元一次不定方程:

a1x1+a2x2++anxn=c

当且仅当 cgcd1inai 的倍数时有解。

对于本题,设答案为 ans,则有下式成立:

ns+n(n+1)2d+1inaians(modm)

ans 为满足上式的最小非负整数。

a=n,b=n(n+1)2,c=m,sum=iai。套路地展开一下,实际上即要求构造 x,y,z,满足:

ax+by+cz+sum=ans

发现 ax+by+cz 是一个不定方程形式,由裴蜀定理可知其取值一定为 ans=gcd(a,b,c) 的倍数。则可知 ans 的最小值为:

ans=sumsumgcd(a,b,c)×gcd(a,b,c)

然后考虑如何构造一组合法的非负整数解。考虑先求得不定方程 ax+by=gcd(a,b) 的一组解 x1,y1,再求得 gcd(a,b)x+cy=gcd(a,b,c) 的一组解 x2,y2,由上式则有一组解为:

x=x1x2sumgcd(a,b,c)y=y1x2sumgcd(a,b,c)

然后考虑把 x,y 调整为非负。发现此题是在模 m 意义下,则仅需加减模 m 即可,太方便啦!

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
//=============================================================
int n;
LL m, sum;
//=============================================================
LL exgcd(LL a_, LL b_, LL &x_, LL &y_) {
if (!b_) {
x_ = 1, y_ = 0;
return a_;
}
LL d_ = exgcd(b_, a_ % b_, y_, x_);
y_ -= a_ / b_ * x_;
return d_;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
int a; std::cin >> a;
sum = (sum + a) % m;
}
LL x1, y1;
LL d1 = exgcd(n, 1ll * n * (n + 1) / 2ll, x1, y1);
x1 = (x1 % m + m) % m, y1 = (y1 % m + m) % m;
LL x2, y2;
LL d2 = exgcd(d1, m, x2, y2);
x2 = (x2 % m + m) % m;
LL k = -sum / d2;
x1 = (x1 * k % m * x2 % m + m) % m;
y1 = (y1 * k % m * x2 % m + m) % m;
std::cout << sum + k * d2 << "\n" << x1 << " " << y1 << "\n";
return 0;
}

G

手玩,树哈希。

大力手玩题。

首先发现若图中有多于一个环则必定为不合法,此时断开环上不同的边必然导致不同构。

于是仅需考虑基环树的情况。考虑以环上 k 个节点为根的子树分别为 t0,t1,,tk1,记 ti=tj 表示 ti,tj 同构。

手玩下发现若环为奇环,当且仅当 t0=t1==tk1 时合法;若为偶环,记环上节点 i 在环上对称节点为 fj(两条路径 ij 长度相同,均为 k2),则当且仅当基环树以任意 (i,fj) 为轴均呈现轴对称时合法,再手玩下发现此时一定有 ti=t(i+2)modk

为什么想到轴对称?因为想到需要令不同的边断开均是等价的,根据基环树的形态想到断开对称的边一定需要是等价的,于是想到对于任意对称轴对称的边均应等价。

按照上述结论,按顺序枚举环上所有节点的子树,并判断对应位置是否同构,树哈希实现即可。

code by dztle:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ull unsigned long long
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;
while(s>='0'&&s<='9') x=(x<<3)+(x<<1)+(s^'0'),s=getchar();
return x*f;
}
const int N=1e5+5;
ull pri[N];
int T,n,m,tot,head[N];
struct node{
int to,nxt;
}e[N*10*2];
int uu[N],vv[N];
void add(int u,int v){
e[++tot].nxt=head[u],head[u]=tot;
e[tot].to=v;
}
int fa[N],top;
int q[N];
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x!=y) fa[x]=y;
}
bool ok[N];
int que[N],cnt;
void dfsh(int u,int fap,int g){
if(ok[g]) return;
q[++top]=u;
if(u==g){
cnt=0;
for(int i=1;i<=top;++i){
ok[q[i]]=1;
que[++cnt]=q[i];
}
return;
}
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fap) continue;
if(fap==0&&v==g) continue;
dfsh(v,u,g);
}
--top;
}
int siz[N];
ull get(int u,int fap){
ull ans=1;
siz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fap) continue;
if(ok[v]) continue;
ans+=get(v,u)*pri[siz[v]];
siz[u]+=siz[v];
}
return ans;
}
signed main(){
srand(time(0));
for(int i=1;i<=100000;++i){
pri[i]=i*i+rand();
if(!(pri[i]&1)) pri[i]++;
}
T=read();
while(T--){
cnt=0;
n=read(),m=read();
tot=0;
for(int i=1;i<=n;++i){
head[i]=0;
}
for(int i=1,u,v;i<=m;++i){
u=read(),v=read();
add(u,v),add(v,u);
uu[i]=u,vv[i]=v;
}
if(m==n-1){
puts("YES"); continue;
}
if(m>n) {
puts("NO"); continue;
}
for(int i=1;i<=n;++i){
fa[i]=i;ok[i]=0;
}
top=0;
for(int i=1;i<=m;++i){
int u=find(uu[i]),v=find(vv[i]);
if(u!=v){
merge(u,v);
}else{
// uu[i] vv[i]
dfsh(uu[i],0,vv[i]);
}
}
ull now=0;
bool fl=0;
if(cnt%2==1){
now=get(que[1],0);
for(int i=2;i<=cnt;++i){
if(get(que[i],0)!=now){
fl=1;
}
}
}else{
now=get(que[1],0);
for(int i=3;i<=cnt;i+=2){
if(get(que[i],0)!=now){
fl=1;
}
}
now=get(que[2],0);
for(int i=4;i<=cnt;i+=2){
if(get(que[i],0)!=now){
fl=1;
}
}
}
if(fl){
puts("NO");
}else puts("YES");
}
return 0;
}
/*
3
3 3
1 2
2 3
3 1
4 4
1 2
2 3
3 1
3 4
7 7
1 2
2 3
3 1
1 4
2 5
5 6
3 7
*/

M

换根 DP,线段树,数论。

考虑枚举起点将其作为根节点 R,确定根节点后显然最优的 d 应为 gcd(depc1,depc2,,depck),此时的总花费即为:

2×1ikdepcigcd1ikdepc1

上式的分子是经典的换根 DP 问题,考虑能否在换根的同时维护分母,即维护所有关键点的深度。显然每次换根向子节点沿距离为 w 的边移动时,会令子树中的节点 depw,其他所有节点 dep+w。考虑将所有关键点按 dfs 序排序使子树内关键点构成一段连续区间,移动后维护 dep 可转化为区间修改问题。

那么在此基础上能否求得全局 gcd 呢?答案是可以的。由数论性质可知:

gcd(a1,a2,,ak)=gcd(a1,a2a1,,akak1)

则仅需维护差分数组的 gcd,此时区间修改转化为了差分数组的单点修改,线段树单点修改+维护区间 gcd 实现即可,单次修改时间复杂度为 O(lognlogv) 级别。

每次换根时按上述算法分别维护分子分母的值即可,取最小值即为答案。换根时需要进行常数次线段树修改,则总时间复杂度为 O(nlognlogv) 级别。

注意:在求节点 dfs 序、维护每个节点子树范围、以及维护线段树时应仅考虑关键点,若出现非关键点,则它们也会被区间修改影响从而影响求得的 gcd

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 5e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
int n, k;
int edgenum, head[kN], v[kN << 1], w[kN << 1], ne[kN << 1];
int dfnnum, dfn[kN], node[kN], subtree[kN][2];
LL ans = kInf, dep[kN], f[kN], g[kN];
bool colored[kN];
//=============================================================
namespace Seg {
#define ls ((now_<<1))
#define rs ((now_<<1|1))
#define mid ((L_+R_)>>1)
const int kNode = kN << 2;
LL d[kNode];
void Pushup(int now_) {
d[now_] = std::__gcd(d[ls], d[rs]);
}
void Build(int now_, int L_, int R_) {
if (L_ == R_) {
d[now_] = dep[node[L_]] - dep[node[L_ - 1]];
return ;
}
Build(ls, L_, mid), Build(rs, mid + 1, R_);
Pushup(now_);
}
void Insert(int now_, int L_, int R_, int pos_, LL val_) {
if (pos_ < L_ || pos_ > R_) return ;
if (L_ == R_) {
d[now_] += val_;
return ;
}
if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
else Insert(rs, mid + 1, R_, pos_, val_);
Pushup(now_);
}
LL Query() {
return abs(d[1]);
}
#undef ls
#undef rs
#undef mid
}
void Add(int u_, int v_, int w_) {
v[++ edgenum] = v_;
w[edgenum] = w_;
ne[edgenum] = head[u_];
head[u_] = edgenum;
}
int Dfs1(int u_, int fa_, LL dep_) {
if (colored[u_]) {
node[++ dfnnum] = u_;
dfn[u_] = dfnnum;
dep[u_] = dep_;
g[u_] = 1;
subtree[u_][0] = dfnnum;
} else {
subtree[u_][0] = kN;
}
int min_dfnnum = kN;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (v_ == fa_) continue;
min_dfnnum = std::min(min_dfnnum, Dfs1(v_, u_, dep_ + w_));
f[u_] += f[v_] + 1ll * w_ * g[v_];
g[u_] += g[v_];
}
subtree[u_][0] = std::min(subtree[u_][0], min_dfnnum);
subtree[u_][1] = dfnnum;
return subtree[u_][0];
}
void Dfs2(int u_, int fa_, LL f_, LL g_) {
LL d = Seg::Query();
if (d == 0) ans = 0;
else ans = std::min(ans, 1ll * (f[u_] + f_) / d);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (v_ == fa_) continue;
LL newg = g_ + g[u_] - g[v_];
LL newf = f_ + (f[u_] - (f[v_] + 1ll * g[v_] * w_)) + 1ll * w_ * newg ;
// LL newf = f_ + 1ll * w_ * newg;
Seg::Insert(1, 1, k, 1, w_);
if (subtree[v_][0] <= subtree[v_][1]) {
Seg::Insert(1, 1, k, subtree[v_][0], -2ll * w_);
Seg::Insert(1, 1, k, subtree[v_][1] + 1, 2ll * w_);
}
Dfs2(v_, u_, newf, newg);
Seg::Insert(1, 1, k, 1, -w_);
if (subtree[v_][0] <= subtree[v_][1]) {
Seg::Insert(1, 1, k, subtree[v_][0], 2ll * w_);
Seg::Insert(1, 1, k, subtree[v_][1] + 1, -2ll * w_);
}
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> k;
for (int i = 1; i <= k; ++ i) {
int c; std::cin >> c;
colored[c] = 1;
}
for (int i = 1; i < n; ++ i) {
int u_, v_, w_; std::cin >> u_ >> v_ >> w_;
Add(u_, v_, w_), Add(v_, u_, w_);
}
Dfs1(1, 0, 0);
Seg::Build(1, 1, k);
Dfs2(1, 0, 0, 0);
std::cout << 2ll * ans << "\n";
return 0;
}
/*
7 1
2
1 2 1
2 3 1
3 4 1
4 5 1
5 6 1
6 7 1
5 3
2 3 4
1 2 4
2 3 2
3 4 1
4 5 4
*/

写在最后

学到了什么:

  • A:裴蜀定理可扩展到任意个变量。
  • G:相等关系的推导。
  • M:区间修改区间 gcdO(lognlogv) 做法。
  • 一般调现在手里还没烂掉的题比开新题更有性价比。
posted @   Luckyblock  阅读(435)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示