2019 ICPC 上海

2019 ICPC 上海重现


B.Prefix Code

题意:

给n个长度不超过10的数字串,问是否存在一个串是另一个串的前缀,如果不存在则输出Yes,如果存在就输出No
数据范围:n<=1e4

思路:

字典树。

code:

#include<bits/stdc++.h>
using namespace std;
const int maxm=1e4+5;
struct Node{
    int a[maxm*10][10],tot;
    int cnt[maxm*10];//记录node被经过次数
    int is[maxm*10];//记录串结尾
    void init(){
        for(int i=0;i<=tot;i++){
            memset(a[i],0,sizeof a[i]);
            cnt[i]=is[i]=0;
        }
        tot=0;
    }
    void add(char *s){
        int len=strlen(s);
        int node=0;
        for(int i=0;i<len;i++){
            int v=s[i]-'0';
            if(!a[node][v])a[node][v]=++tot;
            node=a[node][v];
            cnt[node]++;
        }
        is[node]=1;
    }
    int ask(){
        for(int i=1;i<=tot;i++){
            if(is[i]&&cnt[i]>1)return 1;
        }
        return 0;
    }
}t;
char s[15];
signed main(){
    int T;
    scanf("%d",&T);
    for(int cas=1;cas<=T;cas++){
        t.init();
        int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%s",s);
            t.add(s);
        }
        printf("Case #%d: ",cas);
        if(!t.ask())puts("Yes");
        else puts("No");
    }
    return 0;
}

D.Spanning Tree Removal

题意:

给n,表示有一个n个顶点的完全图
每次操作要从中删除一个生成树,问做多删除几次
且输出边的删除方案
数据范围:n<=1e3

思路:

因为完全图一共n(n-1)/2条边,一颗生成树n-1条边,容易想到可以删除n/2次
这题的难点主要在于如何构造删边方案。

做法:
在这里插入图片描述
把n个点围成一个圆,然后按Z字形删,这样删的话圆周的边每次只减少两条,一共n/2次减完
找一下每次删除的点的规律:
假设起始点为x,则第一次删的是x-(x+1),第二次删的是(x+1)-(x-2),第三次是(x-2)-(x+3)…
发现就是当前点先顺时针移动1格,然后逆时针移动两格,然后顺时针移动三格…代码里面有体现

code:

#include<bits/stdc++.h>
using namespace std;
signed main(){
    int T;
    scanf("%d",&T);
    int cas=1;
    while(T--){
        int n;
        scanf("%d",&n);
        printf("Case #%d: %d\n",cas++,n/2);
        for(int i=1;i<=n/2;i++){
            int now=i;
            int nt;
            for(int j=1;j<=n-1;j++){
                if(j&1){
                    nt=(now+j)%n;
                }else{
                    nt=(now-j+n)%n;
                }
                if(!nt)nt=n;
                printf("%d %d\n",now,nt);
                now=nt;
            }
        }
    }
    return 0;
}

E.Cave Escape

题意:

大意:
给n和m表示有一个n*m的矩阵
给起点(stx,sty)和终点(edx,edy)
每个点(x,y)有权值v(x,y),
从每个点(i,j)可以走到相邻的点(x,y),如果是第一次走到,那么总得分会增加两点的权值积v(i,j)*v(x,y)
现在要从起点走到终点,问路径上最大得分是多少,注意到达终点之后可以继续走赚得分,然后再回来
数据范围:T组数据(T<=10),n,m<=1e3,0<=点权<100

思路:

因为点权非负,所以走过所有的点才能得到最大得分,因此起点和终点其实是没用的。
因为每个点只有第一次到的时候才会有得分,相邻格子之间建边,很容易看出来答案其实就是最大生成树

但是总点数1e6,边大概2e6,排序大约4e7,而且是多组数据,有点难顶,很多人都是1700ms左右卡过去的(时限2s)

正解是从数据范围入手,因为点权最大100,那么边权最大1e4,用类似桶的方法来存边,避免排序
具体做法是用vector+pair开的g(x)存边权为x的边,因为边权最大1e4,因此开1e4大小就行了,从大到小遍历,不需要排序
最大生成树就是常规做法,关键在于用桶存边减少复杂度

总结:当需要给边排序且边权较小时,可以用桶来存边避免排序

code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxm=1e6+5;
vector<pair<int,int> >g[10000+5];
int pre[maxm];
int x[maxm];
int n,m,stx,sty,edx,edy;
int A,B,C,P;
int ffind(int x){
    return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
int id(int i,int j){
    return (i-1)*m+j;
}
signed main(){
    int T;
    scanf("%d",&T);
    for(int cas=1;cas<=T;cas++){
        scanf("%d%d%d%d%d%d",&n,&m,&stx,&sty,&edx,&edy);
        scanf("%d%d%d%d%d%d",&x[1],&x[2],&A,&B,&C,&P);
        printf("Case #%d: ",cas);
        for(int i=3;i<=n*m;i++){
            x[i]=(A*x[i-1]+B*x[i-2]+C)%P;
        }
        for(int i=0;i<=10000;i++)g[i].clear();
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                if(i+1<=n){
                    int a=id(i,j);
                    int b=id(i+1,j);
                    g[x[a]*x[b]].push_back({a,b});
                }
                if(j+1<=m){
                    int a=id(i,j);
                    int b=id(i,j+1);
                    g[x[a]*x[b]].push_back({a,b});
                }
            }
        }
        for(int i=1;i<=n*m;i++)pre[i]=i;
        int tot=0;
        ll ans=0;
        for(int i=10000;i>=0;i--){
            for(auto v:g[i]){
                int x=ffind(v.first);
                int y=ffind(v.second);
                if(x!=y){
                    ans+=i;
                    pre[x]=y;
                    tot++;
                }
            }
            if(tot==n*m-1)break;
        }
        printf("%lld\n",ans);
    }
    return 0;
}

F.A Simple Problem On A Tree

题意:

给一棵n个节点的树,有q次操作:
操作有4种:
1.将u,v路径上的点权改为w
2.将u,v路径上的点权加上w
3.将u,v路径上的点权乘上w
4.查询u,v路径上的点权立方和

思路:

显然树剖,线段树维护:和,平方和,立方和,乘法标记和加法标记。
在这里插入图片描述


H.Tree Partition

题意:

给一颗n个顶点的树,和一个整数k
每个点有点权
要求切断k-1条边,将树分成k个连通块
每个连通块的权值为连通块内的点的点权和
问最优切割下,最大连通块的权值的最小值是多少

数据范围:n<=1e5,k<=n

思路:

最大值最小化要往二分的方向想
显然最大连通块的权值满足单调性
因此二分枚举最大连通块的权值,判断能否成立即可

如何判断权值mid能否成立:
对于某个几点,先将所有子节点所在连通块全部选中,如果总和大于mid,则必须切割
给子节点连通块从大到小排序,贪心地优先切割权值大的子连通块,切割的时候记录切割次数
如果切割总次数小于等于k-1则满足条件

详见代码

code:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=1e5+5;
int head[maxm],nt[maxm<<1],to[maxm<<1],tot;
int a[maxm];
int n,k;
int mid;
int kk;
void init(int n){
    for(int i=1;i<=n;i++)head[i]=0;
    tot=0;
}
void add(int x,int y){
    tot++;nt[tot]=head[x];head[x]=tot;to[tot]=y;
}
bool cmp(int a,int b){
    return a>b;
}
int dfs(int x,int fa){
    int sum=a[x];
    vector<int>temp;
    for(int i=head[x];i;i=nt[i]){
        int v=to[i];
        if(v==fa)continue;
        int t=dfs(v,x);
        temp.push_back(t);
        sum+=t;
    }
    if(sum<=mid)return sum;
    sort(temp.begin(),temp.end(),cmp);
    for(int v:temp){//贪心的从大到小切割
        sum-=v;
        kk++;
        if(sum<=mid)break;
    }
    return sum;
}
bool check(){
    kk=0;
    dfs(1,-1);
    return kk<=k-1;
}
signed main(){
    int T;
    cin>>T;
    signed cas=1;
    while(T--){
        cin>>n>>k;
        init(n);
        for(int i=1;i<n;i++){
            int a,b;
            cin>>a>>b;
            add(a,b);
            add(b,a);
        }
        int sum=0;
        int ma=0;
        for(int i=1;i<=n;i++){
            cin>>a[i];
            sum+=a[i];
            ma=max(ma,a[i]);
        }
        int ans=0;
        int l=ma,r=sum;
        while(l<=r){
            mid=(l+r)/2;
            if(check())ans=mid,r=mid-1;
            else l=mid+1;
        }
        printf("Case #%d: ",cas++);
        cout<<ans<<endl;
    }
    return 0;
}

K.Color Graph

题意:

给一个n个点m条边的简单图,简单图的定义是无向图,没有自环和重边。
现在你要选出来一些边,满足选出来的边不构成奇环。
问最多可以选择多少条边。
数据范围:n<=16,m<=n*(n-1)/2

思路:

无奇环的图是二分图,二进制枚举点集的左半部,利用左半部推出右半部,对边数取max就是答案。
因为最优解一定是二分图,所以这样枚举一定会枚举到答案。

code:

#include<bits/stdc++.h>
using namespace std;
const int maxm=20;
vector<int>g[maxm];
int mark[maxm];
signed main(){
    int T;
    scanf("%d",&T);
    for(int cas=1;cas<=T;cas++){
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)g[i].clear();
        for(int i=1;i<=m;i++){
            int a,b;
            scanf("%d%d",&a,&b);
            a--,b--;//把下标改成从0开始
            g[a].push_back(b);
            g[b].push_back(a);
        }
        int ans=0;
        for(int i=0;i<(1<<n);i++){//二进制枚举左半部
            for(int j=0;j<n;j++){//标记选中的点
                mark[j]=(i>>j&1);
            }
            int temp=0;
            for(int j=0;j<n;j++){//计算边
                if(i>>j&1){
                    for(int v:g[j]){
                        if(!mark[v])temp++;//如果另一端没被标记说明另一端可以作为右半部,则边可选
                    }
                }
            }
            ans=max(ans,temp);
        }
        printf("Case #%d: %d\n",cas,ans);
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章