2021 ICPC 亚洲区域赛上海站VP题解

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 qt2pt+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) (n1)的树,将所有边两两分成 n − 1 2 \frac{n-1}{2} 2n1组,并且同一组内的边必须连到同一个点上,求不同的划分种类数

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]=(k1)!!Π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].disval[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 AB两组,使值得 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][i1][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]][i1][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[wt[i]][i1][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]][i1][j1]+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[w2t[i]][i1][j1]+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 t1个黑点每个和这 t t t个白点连边的权值都是 1 n t \frac{1}{nt} nt1,这些白点剩下的 1 n t \frac{1}{nt} nt1权值平分给剩下的 n + 1 − t n + 1 − t n+1t个黑点,则⼀定有⼀个黑点的匹配边权值是 1 n ( n + 1 − t ) t \frac{1}{n(n+1-t)t} n(n+1t)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 k1个左部点全部连满 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} n2n+12n+21就是答案,代码懒得贴了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值