2021 ICPC 亚洲区域赛上海站VP题解
2023赛季打完了,先把之前跟队友疯狂VP但没补的场次标一下,然后滚去准备期末考试,考完试再来好好研究
A(可补)
B(容斥/NTT,有需要就补)
C(可补)
D
题目大意
给定 p q \frac {p}{q} qp,找到两个正整数满足 p q = a b + b a \frac {p}{q}=\frac{a}{b}+\frac{b}{a} qp=ba+ab
1 < = p , q < = 1 e 7 1<=p,q<=1e7 1<=p,q<=1e7
思路
非常简单的想法:直接设 t = a b t=\frac {a}{b} t=ba
那么问题转化为 q t 2 − p t + q = 0 qt^2-pt+q=0 qt2−pt+q=0是否有有理数根的问题,判断判别式即可
另一种做法
代码如下
#include<bits/stdc++.h>
#define int long long
using namespace std;
int gcd(int a,int b)
{
while(b)
{
int t=a%b;
a=b;
b=t;
}
return a;
}
void work()
{
int p,q;
cin>>p>>q;
int delta=p*p-4*q*q;
int s_delta=sqrt(delta);
if(s_delta*s_delta==delta)
{
int b=p+s_delta,a=2*q;
int d=gcd(b,a);
b/=d;a/=d;
cout<<a<<' '<<b<<'\n';
}
else
cout<<"0 0\n";
}
signed main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
cin>>t;
while(t--)
work();
return 0;
}
复杂一点的数论做法
自己写的时候一开始想到这样做,挂了好多发
先将左边约分,右边通分 a 2 + b 2 a b \frac{a^2+b^2}{ab} aba2+b2
由于约分之后分子分母互质,所以直接令 q = a b q=ab q=ab
对于 q q q的每个质因子,二进制枚举它属于 a a a还是属于 b b b,再判断 a 2 + b 2 = p a^2+b^2=p a2+b2=p即可,因为 a 2 + b 2 a^2+b^2 a2+b2也与 a b ab ab互质,所以不可能有一个质因子既属于 a a a又属于 b b b
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
struct node
{
int di;int mi;
}fac[maxn];
int gcd(int a,int b)
{
return (b?gcd(b,a%b):a);
}
int qpow(int a,int b)
{
int res=1;
while(b)
{
if(b&1) res*=a;
b>>=1;
a*=a;
}
return res;
}
void work()
{
int p,q,tot=0;
memset(fac,0,sizeof fac);
cin>>p>>q;
int d=gcd(p,q);
p/=d;q/=d;
int tmp=q;
for(int i=2;i*i<=q;i++)
{
if(tmp%i==0) fac[tot++].di=i;
while(tmp%i==0)
{
fac[tot-1].mi++;
tmp/=i;
}
}
if(tmp>1) fac[tot++].di=tmp,fac[tot-1].mi=1;
for(int sta=0;sta<1<<tot;sta++)
{
int a=1,b;
for(int i=0;i<tot;i++)
if(sta>>i&1) a*=qpow(fac[i].di,fac[i].mi);
b=q/a;
if(a*a+b*b==p)
{
cout<<a<<' '<<b<<'\n';
return;
}
}
cout<<"0 0\n";
}
signed main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
cin>>t;
while(t--)
work();
return 0;
}
需要注意的点
数组从零开始计数,插入时tot++,
只是把tot清空了,以为其它不用清,但有一个域是累加值
E
题目大意
给定一个长度为 n n n的数组,给定一个正整数 k k k。要求选择尽可能多的数,使得选出的数两两之间大于等于 k k k
1 < = n < = 1 e 5 1<=n<=1e5 1<=n<=1e5
思路
显然与原数组的次序无关,于是将数组从小到大排序,贪心地选择即可
代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int a[maxn];
void work()
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i];
sort(a+1,a+n+1);
int ans=1,last=a[1],i=1;
for(;;)
{
while(a[i]-last<k&&i!=n+1) i++;
if(i>n) break;
if(a[i]-last>=k) ans++;
last=a[i];
}
cout<<ans<<'\n';
}
int main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
//cin>>t;
while(t--)
work();
return 0;
}
F(博弈,有需要就补)
G
题目大意
给定一棵有偶数条边 ( n − 1 ) (n-1) (n−1)的树,将所有边两两分成 n − 1 2 \frac{n-1}{2} 2n−1组,并且同一组内的边必须连到同一个点上,求不同的划分种类数
1 < = n < = 1 e 5 1<=n<=1e5 1<=n<=1e5
思路
树形DP,考虑奇偶条件的限制就比较好划分状态了
如果某个点的子树中边有偶数个,那么这些边一定都被恰好划分完
如果某个点的子树中边有奇数个,那么一定有它的一个儿子边与它的祖先边分到一组
于是设 d p [ u ] dp[u] dp[u]为以 u u u为根的子树的划分方案数
当 u u u子树中有奇条边时,表示将它的前驱边算入的方案数
当 u u u子树中有偶条边时,表示不将它的前驱边算入的方案数
设 k k k为 u u u中偶边儿子的个数,则
d p [ u ] = ( k − 1 ) ! ! Π d p [ v ] ( k 为偶数 ) dp[u]=(k-1)!!\Pi dp[v] (k为偶数) dp[u]=(k−1)!!Πdp[v](k为偶数)
d p [ u ] = k ! ! Π d p [ v ] ( k 为奇数 ) dp[u]=k!!\Pi dp[v](k为奇数) dp[u]=k!!Πdp[v](k为奇数)
需要注意的点
PS:将n个物品两两分组的总方案数为(n-1)!!
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
const int mod=998244353;
struct Edge
{
int next;int to;
}edge[maxn<<1];
int head[maxn],cnt,n;
int siz[maxn],dp[maxn],k[maxn];
int dj[maxn];
void addedge(int from,int to)
{
edge[++cnt].next=head[from];
edge[cnt].to=to;
head[from]=cnt;
}
void dfs1(int u,int fat)
{
siz[u]=1ll;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fat) continue;
dfs1(v,u);
if(siz[v]&1ll) k[u]++;
siz[u]+=siz[v];
}
}
void dfs(int u,int fat)
{
if(k[u]&1) dp[u]=dj[k[u]];
else if(k[u]!=0) dp[u]=dj[k[u]-1];
else dp[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fat) continue;
dfs(v,u);
dp[u]=((long long)dp[u]*dp[v])%mod;
}
}
void work()
{
cin>>n;
for(int i=1;i<n;i++)
{
int x,y;
cin>>x>>y;
addedge(x,y);
addedge(y,x);
}
dfs1(1,0);
dfs(1,0);
cout<<dp[1]<<'\n';
}
void Pre()
{
dj[0]=dj[1]=1;dj[2]=2;
for(int i=3;i<maxn;i++)
dj[i]=((long long)i*dj[i-2])%mod;
}
signed main()
{
Pre();
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
//cin>>t;
while(t--)
work();
return 0;
}
H
题目大意
给定一个 n n n个点 m m m条边的无向连通图,每个点有点权 a i a_i ai,经过该点可以使得总钱数增加 a i a_i ai(每个点只能加一次),只有钱数超过某条边的限制 w i w_i wi时,才可通过此边(不扣钱)
q q q次询问,给定 x , k x,k x,k,问起点在 x x x且初始钱数为 k k k时,最后所能攒下的最大钱数是多少
1 < = n , m , q < = 1 e 5 1<=n,m,q<=1e5 1<=n,m,q<=1e5 其他数据均为正整数。
思路
kruskal重构树
原图中两个点之间的所有简单路径上最大边权的最小值 = 最小生成树上两个点之间的简单路径上的最大值 = Kruskal 重构树上两点之间的 LCA 的权值。
显然在最小生成树对应的kruskal重构树上,越往根节点走,点的权值越大,到了某个点就一定能通过这个点的所有子树
那么问题就转化成了,求点 x x x在重构树上能够爬到的最高的点 u u u,我们预处理 v a l [ u ] val[u] val[u]为 u u u的子树上的点权和,最终答案就是 v a l [ u ] + k val[u]+k val[u]+k
将倍增初始值设为 d p [ u ] [ 0 ] = e [ i ] . d i s − v a l [ u ] dp[u][0]=e[i].dis-val[u] dp[u][0]=e[i].dis−val[u]
因为能够到达节点 u u u,那么一定会把它子树上的点的钱都赚一遍再考虑能不能向上爬
代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=4e5+5;
typedef long long ll;
struct Edge
{
int from;int to;int dis;
}edge[maxn];
bool cmp(Edge x,Edge y)
{
return x.dis<y.dis;
}
int m,n,q,cnt;
int f[maxn][25],fat[maxn];
ll dp[maxn][25],val[maxn];
int find(int x)
{
if(fat[x]==x) return x;
return fat[x]=find(fat[x]);
}
void kruskal()
{
for(int i=1;i<=n;i++) fat[i]=i;
int tot=0;
cnt=n;
for(int i=1;tot<n-1;i++)
{
int f1=find(edge[i].from),f2=find(edge[i].to);
if(f1!=f2)
{
cnt++;fat[cnt]=cnt;
val[cnt]=val[f1]+val[f2];
f[f1][0]=f[f2][0]=fat[f1]=fat[f2]=cnt;
dp[f1][0]=edge[i].dis-val[f1];
dp[f2][0]=edge[i].dis-val[f2];
tot++;
}
}
}
void work()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++) cin>>val[i];
for(int i=1;i<=m;i++)
cin>>edge[i].from>>edge[i].to>>edge[i].dis;
sort(edge+1,edge+m+1,cmp);
kruskal();
val[0]=val[cnt];
for(int j=1;j<=20;j++)
for(int i=1;i<=cnt;i++)
{
f[i][j]=f[f[i][j-1]][j-1];
dp[i][j]=max(dp[i][j-1],dp[f[i][j-1]][j-1]);
}
while(q--)
{
int x,k;
cin>>x>>k;
for(int i=20;i>=0;i--) while(dp[x][i]<=k&&x) x=f[x][i];
cout<<val[x]+k<<'\n';
}
}
int main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
//cin>>t;
while(t--)
work();
return 0;
}
I
题目大意
给定 n n n个物品,和一个正整数 k k k。每个物品有两个权值 v i , t i v_i,t_i vi,ti,你可以将至多 k k k个物品的 t i t_i ti值翻倍。
现在要从这 n n n个物品中任意选出一些物品分为 A 、 B A、B A、B两组,使值得 t i t_i ti的总和相等,求最大的 v i v_i vi的总和
n < = 100 , k < = n , t i < = 13 , v i < = 1 e 9 n<=100,k<=n,t_i<=13,v_i<=1e9 n<=100,k<=n,ti<=13,vi<=1e9
思路
分为两组,那么可以将 A A A组的 t i t_i ti看为正, B B B组的 t i t_i ti看为负,那么就转化为了一个01背包问题
设 d p [ w ] [ i ] [ j ] dp[w][i][j] dp[w][i][j]为考虑了前 i i i个物品,当前 ∑ t \sum t ∑t为 w w w,已经使用了 j j j次翻倍后的 ∑ v \sum v ∑v的最大值
转移方程很显然
d p [ w ] [ i ] [ j ] dp[w][i][j] dp[w][i][j]可以从以下状态中转移
d p [ w ] [ i − 1 ] [ j ] dp[w][i-1][j] dp[w][i−1][j]不选择第 i i i个物品
d p [ w + t [ i ] ] [ i − 1 ] [ j ] + v [ i ] dp[w+t[i]][i-1][j]+v[i] dp[w+t[i]][i−1][j]+v[i],选择第 i i i个物品,加入 A A A组中
d p [ w − t [ i ] ] [ i − 1 ] [ j ] + v [ i ] dp[w-t[i]][i-1][j]+v[i] dp[w−t[i]][i−1][j]+v[i],选择第 i i i个物品,加入 B B B组中
d p [ w + 2 t [ i ] ] [ i − 1 ] [ j − 1 ] + v [ i ] dp[w+2t[i]][i-1][j-1]+v[i] dp[w+2t[i]][i−1][j−1]+v[i],选择第 i i i个物品,并翻倍,加入 A A A组中
d p [ w − 2 t [ i ] ] [ i − 1 ] [ j − 1 ] + v [ i ] dp[w-2t[i]][i-1][j-1]+v[i] dp[w−2t[i]][i−1][j−1]+v[i],选择第 i i i个物品,并翻倍,加入 B B B组中
显然可以使用滚动数组来优化。
注意如果按原转移方程,下标可能为负,我们通过加一个offset变量来防止越界
代码如下
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
const int inf=0x3fffffff;
int dp[20005][2][101];
int t[maxn],val[maxn],n,k;
void work()
{
memset(dp,0x80,sizeof dp);
int ans=-inf;
cin>>n>>k;
int offset=0;
for(int i=1;i<=n;i++)
{
cin>>val[i]>>t[i];
if(t[i]<0) offset+=-t[i];
else offset+=t[i];
}
dp[offset][0][0]=0;
for(int i=1;i<=n;i++)
for(int w=0;w<=offset*2;w++)
for(int j=0;j<=k;j++)
{
dp[w][i&1][j]=dp[w][(i-1)&1][j];
if(w>=t[i]) dp[w][i&1][j]=max(dp[w][i&1][j],dp[w-t[i]][(i-1)&1][j]+val[i]);
if(w>=2*t[i]&&j>=1) dp[w][i&1][j]=max(dp[w][i&1][j],dp[w-t[i]*2][(i-1)&1][j-1]+val[i]);
if(w+t[i]*2<20005&&j>=1) dp[w][i&1][j]=max(dp[w][i&1][j],dp[w+t[i]*2][(i-1)&1][j-1]+val[i]);
if(w+t[i]<20005) dp[w][i&1][j]=max(dp[w][i&1][j],dp[w+t[i]][(i-1)&1][j]+val[i]);
if(w==offset&&i==n) ans=max(ans,dp[w][i&1][j]);
}
cout<<ans<<'\n';
}
signed main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
//cin>>t;
while(t--)
work();
return 0;
}
J(可补)
K
题目大意
一个长度为n的01串,1表示上面有光子,每个光子在1s后向左右分裂成两个光子,光子之间碰撞会湮灭,最末端的光子分裂会遗失一部分。构造一个局面使得2n秒后01串上不为0
思路
找到可以拼接的且循环周期相同的循环串,如1001和10001,并且它们后面接上10都满足这个性质
那么也就是4k+5b+2或4k+5b。实际上只有3无解
代码如下
#include<bits/stdc++.h>
using namespace std;
void work()
{
int n;
cin>>n;
if(n==3)
{
cout<<"Unlucky\n";
return;
}
if(n%2==1)
{
cout<<"10001";
n-=5;
}
while(n>=4) cout<<"1001",n-=4;
if(n) cout<<"10";
cout<<"\n";
}
int main()
{
cin.tie(0);
cin.sync_with_stdio(0);
int t=1;
//cin>>t;
while(t--)
work();
return 0;
}
M
题目大意
有一块面积为1的地,将它划分为 n n n块,设A和S为两种不同的均分它的方式。
求一组划分A,S,使得A和S中所有块的组合的交叉面积中的最大值最小,求这个交叉面积
思路
题目拿过来之后懵了老半天,读了4、5遍没读懂题目要干什么。赛后补题跟undo队讨论了一下,说就一个式子,但是题面确实抽象
关键点:对于每个划分S中的块,必定会被划分A中的x个块覆盖,重合面积和为1/n,对于每个划分A中的块也亦然
于是可以转化为一个二分图,每个点向右边的所有点连边,每个点的边权之和为1/n,求完备匹配中最小边权的最大值。网络流可以做
题解给了一个神奇的构造上界:
“考虑如下构造产生的答案的⼀个上界:
取 t t t个白点,令前 t − 1 t-1 t−1个黑点每个和这 t t t个白点连边的权值都是 1 n t \frac{1}{nt} nt1,这些白点剩下的 1 n t \frac{1}{nt} nt1权值平分给剩下的 n + 1 − t n + 1 − t n+1−t个黑点,则⼀定有⼀个黑点的匹配边权值是 1 n ( n + 1 − t ) t \frac{1}{n(n+1-t)t} n(n+1−t)t1”
不明觉厉
后面去补了一些二分图的定理
霍尔定理:对于一个二部图G~(X,Y),X存在一个匹配的充分必要条件为对于X的任意子集S,S的邻居个数N(S)必须大于等于S的大小|S|。
那么很明显,对于左侧只有一个点由抽屉原理就是 1 n 2 \frac{1}{n^2} n21
假设右边匹配点的个数为 k k k,那么 k − 1 k-1 k−1个左部点全部连满 1 n \frac{1}{n} n1的权值时所能得到的最小权值的最大值最小。剩下就是抽屉原理去计算了
最后推出式子 1 n ⌊ n + 1 2 ⌋ ⌊ n + 2 2 ⌋ \frac{1}{n\lfloor \frac{n+1}{2} \rfloor \lfloor \frac{n+2}{2} \rfloor} n⌊2n+1⌋⌊2n+2⌋1就是答案,代码懒得贴了