The 2021 ICPC Asia Shanghai Regional Programming Contest 2021上海 (8/13)补题

D. Strange Fractions 数论签到

E. Strange Integers 签到,贪心排序

G. Edge Groups 组合数学,树形dp,思维,双阶乘

H. Life is a Game 克鲁斯卡尔重构树 ,倍增,并查集

I. Steadily Growing Steam DP,状态跳转

J. Two Binary Strings Problem   Bitset 思维

K. Circle of Life zz构造

M. Harmony in Harmony  zz构造

p/q = (a^2+b^2)/ab  数论做法枚举ab的时候,复杂度其实是不对的,加特判会擦边水过。

可以直接让b=2q,a直接等于上面分子,也就是判断是否是平方数

#include <bits/stdc++.h>
using namespace std;
typedef long long int ll;

int pos[10000000+10];
int a[10000000+10];
int main(){


   for(int i=1;i*i<=10000000;i++)
   {
       pos[i*i]=i;
       a[i]=i*i;
   }
   int t;
   cin>>t;
   while(t--)
   {
       int p,q;
       scanf("%d%d",&p,&q);

       if(p<2*q)
       {
           cout<<0<<" "<<0<<'\n';
           continue;
       }
       int g=__gcd(p,q);
       p/=g;
       q/=g;
       int flag=0;
       for(int i=1;a[i]<p;i++)
       {
           int fuck=p-a[i];
           if(pos[fuck])
           {
               if((ll)pos[fuck]*(ll)i==q)
               {
                   flag=1;
                   cout<<pos[fuck]<<" "<<i<<'\n';
                   break;
               }
           }
       }
       if(!flag)
       {
           cout<<0<<" "<<0<<'\n';
       }
   }
    return 0;
}
 

 

 

E. Strange Integers

选择m个,任意差值都要>=k,也就是只需要满足大系大小关系紧邻的全部都>=k即可。

直接排序,贪心选择即可 

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
ll a[100000+10];
int main()
{


    ll n,k;
    cin>>n>>k;

    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }

    sort(a+1,a+1+n);
    ll pre=a[1];

    int ans=1;
    for(int i=2;i<=n;i++)
    {
        if(a[i]>=pre+k)
        {
            ans++;
            pre=a[i];
          //  cout<<a[i]<<" ";
        }
    }

    cout<<ans;

    return 0;
}

 G. Edge Groups

一个关键结论是,节点i的子树若有偶数条边,那么一定要把这些边两两配对。否则全局配对就会破坏。而如果有奇数条边,也只能拽过去i和i父亲连接的边来弥补。

所以问题就变成了,统计儿子节点的匹配情况,如果剩一条边没有匹配,那么这条边就锁死。否则,对剩下的自由边进行两两匹配,这个用到组合数学一点知识,方案数是双阶乘,1*3*5...。如果当前自由边是偶数个,就必须匹配完,否则同样会对父亲造成影响。如果是奇数,就一定会剩下一个。只能通过拽去父亲和自己的边来弥补。



# include<bits/stdc++.h>
using namespace std;
typedef long long int ll;
ll fac[100000+10];
# define mod 998244353

void init()
{

    fac[1]=1;
    fac[0]=1;
    for(int i=2;i<=100000;i++)
    {
        if(i%2==1)
        {
            fac[i]=fac[i-1]*(ll)i%mod;
        }
        else
        {
            fac[i]=fac[i-1];
        }
    }
}
int sizeson[100000+10];
ll dp[100000+10];
vector<int>v[100000+10];

void dfs(int now,int pre)
{
     //sizeson 存的是当前边的数量
     int flag=0;
     ll nowans=1;
     int nowtot=0;
     dp[now]=1;
     for(auto it:v[now])
     {
         if(it==pre)
            continue;
         flag=1;
         dfs(it,now);
         if(sizeson[it]%2==0)
         {
             nowtot++;
         }
         nowans*=dp[it];
         nowans%=mod;


     }

     dp[now]=nowans*fac[nowtot];

     dp[now]%=mod;

     sizeson[now]=nowtot%2;

     if(flag==0)
     {
         sizeson[now]=0;
         dp[now]=1;
     }
}
int main()
{

    init();
    int n;
    cin>>n;
    for(int i=1;i<n;i++)
    {
         int x,y;
         scanf("%d%d",&x,&y);
         v[x].push_back(y);
         v[y].push_back(x);
    }

    dfs(1,0);

    cout<<dp[1];

    return 0;
}

Life is a Game

套路先跑重构树,但是建边有讲究。

每次合并两个单点x,y,就设置一个虚点1,权值为a[x]+a[y].边权分别是w-a[x],w-a[y],w为x,y之间边权。

又有两个单点a,b, 设置虚点2,权值为a[a]+a[b],边权等等,

若合并虚点1,虚点2,边权为分别为w-a[a]-a[b]  ,w-a[x]-a[y] ,虚点3,为合并虚点1,2的结果

那么,我们从单点x到虚点3的路径,有两条边,边权分别为w-a[x],w-a[x]-a[y]

若k>=w-a[x]意味着我们可以到达y,当k>=w-a[x]-a[y],意味着我们可以通过x,y集合和a,b集合的一条边到达a,b中的某一个,然而,我们重构树连边过程中,边权从小到大,在合并两个虚点的时候,已经合并了虚点管辖的单点,意味着k>=w-a[x]-a[y]的时候,一定能到达a,b二者。

然后就是重构树常规操作。对给定点x进行倍增的时候,倒叙遍历,只要能跳就跳,越往上,a[i]权值之和越高。

#include <bits/stdc++.h>

using namespace std;
typedef long long int ll;
# define mod 1000000007

ll a[200000+10];
int f[200000+10];
int getf(int x)
{
    if(x==f[x])return x;
    else
    {
        f[x]=getf(f[x]);
        return f[x];
    }
}
struct node
{
    int b,e;
    ll w;
};
struct node s[200000+10];
bool cmp(struct node x, struct node y)
{
    return x.w<y.w;
}
ll maxx[200000+10][31];
int ans[200000+10][31],m,n,tot;
void work()
{


    for(int i=1;i<=m;i++)
    {
        int t1=getf(s[i].b),t2=getf(s[i].e);
        if(t1==t2)
            continue;
        tot++;
        a[tot]=a[t1]+a[t2];
        f[t1]=tot;
        f[t2]=tot;
        ans[t1][0]=tot;
        ans[t2][0]=tot;

        maxx[t1][0]=s[i].w-a[t1];
        maxx[t2][0]=s[i].w-a[t2];



    }
}

int main()
{

    int q;
    cin>>n>>m>>q;

    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        f[i]=i;
    }
    for(int i=n+1;i<=2*n;i++)
    {
        f[i]=i;
    }

    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%lld",&s[i].b,&s[i].e,&s[i].w);
    }
    tot=n;
    sort(s+1,s+1+m,cmp);

    work();
    a[0]=a[tot];
    for(int j=1;j<=30;j++)
    {
        for(int i=1;i<=tot;i++)
        {
            maxx[i][j]=max(maxx[ans[i][j-1]][j-1],maxx[i][j-1]);
            ans[i][j]=ans[ans[i][j-1]][j-1];
        }
    }

    while(q--)
    {
        int x,k;
        scanf("%d%d",&x,&k);
        for(int i=30;i>=0;i--)
        {
            if(maxx[x][i]<=k)
            {
                x=ans[x][i];
            }
        }

        cout<<k+a[x]<<'\n';
    }

    return 0;
}

 I. Steadily Growing Steam

dp[0/1][i][j] 代表差值为i(较大者减去较小)并且选了k个*ti的最大v之和。

细节还是不少的。

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
const int N = 2607, M = 107;
ll v[N], t[N];
ll dp[3][N][M];
int main()
{

    int n, kk;
    scanf("%d%d", &n, &kk);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld%lld", &v[i], &t[i]);
    }
    for (int i = 0; i < N; i++)
    {
        for (int j = 0; j <= kk; j++)
        {
            dp[0][i][j] = dp[1][i][j] = -1e17;
        }
    }
    dp[1][0][0] = 0;
    ll mx = 0;
    int now = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= N; j++)
        {
            for (int k = 0; k <= kk; k++)
            {
                dp[now][j][k] = max(dp[now][j][k], dp[!now][j][k]);
            }
            for (int k = 0; k <= kk; k++)
            {
                dp[now][abs(j - t[i])][k] = max(dp[now][abs(j - t[i])][k], dp[!now][j][k] + v[i]);

                if (k < kk)
                {
                    dp[now][abs(j - 2 * t[i])][k + 1] = max(dp[now][abs(j - 2 * t[i])][k + 1], dp[!now][j][k] + v[i]);
                }
                if (j + t[i] < N)
                    dp[now][j + t[i]][k] = max(dp[now][j + t[i]][k], dp[!now][j][k] + v[i]);

                if (k < kk && j + 2 * t[i] < N)
                {
                    dp[now][j + 2 * t[i]][k + 1] = max(dp[now][j + 2 * t[i]][k + 1], dp[!now][j][k] + v[i]);
                }
            }
        }
        now = !now;
    }
    ll res = 0;

    for (int j = 0; j <= kk; j++)
    {
        res = max(res, dp[0][0][j]);
        res = max(res, dp[1][0][j]);
    }
    printf("%lld\n", res);
    return 0;
}

 J. Two Binary Strings Problem

   1当成1,0当成-1,所以当目标值为1的时候,前缀和必须大于0
    当目标值为0的时候,前缀和必须小于等于0
    按照前缀和从大到小排序,设当前位置为i
    b值为1时
    在之前出现的前缀和全都大于等于他(相同的时候按照坐标排序)
    这些j代表的k是不合法的
    b值为0的时候
    在此之前出现的前缀和都大于等于它,大于等于的话肯定对,
    且我们排序规则已经强制了我们已经处理了前缀和等于的情况
    只需要提取出全部没出现的j
    当前前缀和<=0 但是为1的时候,若前面没有比他大的
    当前前缀额>0 但是为0的时候,若后面没有比他小的且靠前的
A是之前处理的下标集合,B是全集,C是错误的k的集合。每次A插入一个下标x,对n-x的位置标记为1,在获取x之前不合法的k时候,右移(n-p)就会把(n-p+1)开始的全部下标变为1开始的,也就是k的长度。这样做发现还是缺乏正确性。原因在于题目一个特殊条件 这个max使我们再统计前缀和的时候,如果一个前缀和(1开始的)不满足bi,那么>=i的k值就1全都不符合了。如果不加这一条件,我们只会把这一个单点给排除,但实际上,以后全部的k都不满足了
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int a[500000+10];
bitset<50010>A,B,C;
int sum[500000+10],id[500000+10];
bool cmp(int x,int y)
{
    if(sum[x]!=sum[y])
    return sum[x]>sum[y];
    return x<y;
}
int main()
{
    int t;
    cin>>t;

    while(t--)
    {
        int n;
        cin>>n;
        string s;
        cin>>s;
        s=" "+s;
        A.reset();
        B.reset();
        C.reset();
        string b;
        cin>>b;
        b= " "+b;
        for(int i=1;i<=n;i++)
        {
            sum[i]=sum[i-1]+1;
            if(s[i]=='0')
                sum[i]-=2;
                id[i]=i;
            B[i]=1;
        }
        B[0]=1;
        id[0]=0;

        sort(id,id+n+1,cmp);
        int minn=1e9;
        for(int i=0;i<=n;i++)
        {
            int p=id[i];

            if(p==0)
            {
                A[n-p]=1;
                continue;
            }

            if(b[p]=='1')
            {
                C|=(A>>(n-p));
            }
            else
            {
                C|=((A^B)>>(n-p));

            }

            if(b[p]=='1'&&sum[p]<=0)
                minn=min(minn,p);
            if(b[p]=='0'&&sum[p]>0)
                minn=min(minn,p);
            A[n-p]=1;
        }

        for(int i=1;i<=n;i++)
        {
            if(C[i]||i>=minn)
            {
                cout<<0;
            }
            else
            {
                cout<<1;
            }
        }

        cout<<'\n';


    }
    return 0;
}
 

 

 K. Circle of Life  M. Harmony in Harmony 

银牌题出俩总共不到五行的构造,不得不说是很不合理的,甚至算得上是出题人的失误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qinsanma and Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值