[造福社会][CEOI2012]5题(除计算几何题)题解(内含详细注释代码)

2 篇文章 0 订阅

前言:

不知道为什么,老师突然让我们做CEOI的题目,还包括写解题报告、出数据、写题面等繁琐的事情。好在我们这套题目没有什么难的不会的算法,最难的计算几何已经被tkj大神解决了,所以总体来说还行。不过写4个SPJ还是比较烦的,想要的可以加我QQ:1553531629。(可能有很多BUG,毕竟第一次写)
还有吐槽:官方数据有错的……标程也有错的……
那么就按照我做题的顺序来写吧。

network

第一个问题比较简单。先tarjan缩点,缩点后图就变为一棵树,因为有一个中心点r可以到达所有点,这个r就为树的根。然后第一问做一次树形DP就可以了,f[x]表示x这个点可以到达的点数,一开始f[x]=x所在连通分量中的点数,然后f[x]再加上所有x儿子y的f[y],就是答案。
第二个问题相对比较难。首先,连的一条边显然是由一个点连向它的某个祖先的,那么一条x连向祖先y的边可以看作删除y到x路径上的所有边,特别地,若一条边的两端点所属同一连通分量,那么这条边视作被删除,问题转化为删除所有边至少要加多少边,注意每条边只能删除一次,否则就无法保证唯一路径这一条件。然后就很好实现了。由于每条边只被删除一次,所以总时间复杂度为O(n+m)。

代码:

#include<cstdio>
#include<cstring>
#include<map>
#include<iostream>
#include<algorithm>
using namespace std;
const int Maxn=100010;
const int Maxm=500010;
int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return x*f;
}
int n,m,r,ex[Maxm],ey[Maxm],fa[Maxn];
struct Edge{int y,next;}e[Maxm],E[Maxm];
int last[Maxn],len=0;
void ins(int x,int y){int t=++len;e[t].y=y;e[t].next=last[x];last[x]=t;}
int Last[Maxn],Len=0;
void Ins(int x,int y){int t=++Len;E[t].y=y;E[t].next=Last[x];Last[x]=t;}
int dfn[Maxn],low[Maxn],id=0,sta[Maxn],top=0,po[Maxn],cnt=0,bel[Maxn],b[Maxn];
//po[i]表示编号为i的连通分量包含的点数,bel[i]表示i点所属的连通分量的编号 
bool in[Maxn];
void Tarjan(int x)
{
    low[x]=dfn[x]=++id;
    sta[++top]=x;in[x]=true;
    for(int i=last[x];i;i=e[i].next)
    {
        int y=e[i].y;
        if(!dfn[y])Tarjan(y),low[x]=min(low[x],low[y]);
        else if(in[y])low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x])
    {
        int i;cnt++;
        do
        {
            b[cnt]=i=sta[top--];
            in[i]=false;
            po[cnt]++;
            bel[i]=cnt;
        }while(i!=x);
    }
}
map<int,bool>h[Maxn];//h[x][y]用来记录连通分量x、y之间是否有边,防止重边 
int f[Maxn],num[Maxn],ans=0,Ans[Maxn][2];
//f[x]表示编号为x的连通分量能到达多少个点,num[x]表示fa[x]到x的这条边的编号 
bool del[Maxm];
void dfs(int x)
{
    f[x]=po[x];//初始值 
    for(int i=Last[x];i;i=E[i].next)
    {
        int y=E[i].y;
        dfs(y);f[x]+=f[y];//y能到达的x也能到达 
    }
}
bool vis[Maxn];
void dfs1(int x)
{
    if(vis[x])return;vis[x]=true;//每个点只能访问一次 
    for(int i=last[x];i;i=e[i].next)dfs1(e[i].y);
    //先让儿子删边 
    if(fa[x]!=-1&&!del[num[x]])
    {
        ans++;Ans[ans][0]=x;
        while(!del[num[x]])//当一条边没有被删除就一直往上删 
        {
            del[num[x]]=true;
            x=fa[x];if(fa[x]==-1)break;
        }
        Ans[ans][1]=x;
    }
}
int main()
{
    memset(fa,-1,sizeof(fa));
    n=read();m=read();r=read();
    for(int i=1;i<=m;i++)
    {
        ex[i]=read(),ey[i]=read();
        ins(ex[i],ey[i]);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])Tarjan(i);//Tarjan缩点 
    for(int i=1;i<=m;i++)//建出缩点后的新图 
    {
        int bx=bel[ex[i]],by=bel[ey[i]];
        if(bx!=by&&!h[bx][by])h[bx][by]=true,Ins(bx,by),fa[ey[i]]=ex[i],num[ey[i]]=i; 
        else if(bx==by)del[i]=true;//默认这些边要被删除 
    }
    dfs(bel[r]);
    //做一次树形DP求出第一问的答案,因为同一个连通分量的点的答案都是一样的,所以一个一个连通分量地算 
    for(int i=1;i<=n;i++)printf("%d ",f[bel[i]]);puts("");
    dfs1(r);//模拟删边过程 
    printf("%d\n",ans);
    for(int i=1;i<=ans;i++)printf("%d %d\n",Ans[i][0],Ans[i][1]);
}

wagons

注意到“每种垃圾最多10种垃圾处理器能处理”,我们可以想到一种方法:枚举能处理第一个垃圾的A,枚举能处理第一个A不能处理的垃圾的B,再枚举能处理第一个A、B都不能处理的垃圾的C,然后枚举A、B、C的先后顺序,再贪心求出答案即可。贪心过程可以看代码。时间复杂度为O(10^3*n),但常数十分小。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define LL long long
const int Maxn=20010;
const int Maxm=1010;
int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return x*f;
}
int n,K,S;
int a[Maxm][12],len[Maxm],b[Maxn],ansA,ansB,ansC,mx=0;//a[i][j]表示能处理第i种垃圾的第j个处理器编号 
bool h[Maxm][Maxm];//h[i][j]=true表示第i个处理器能处理第j种垃圾 
int work(int A,int B,int C)
{//注意到栈中实际上只有两种东西:只能被B处理的(简称为B),和能被C处理的(简称为C),由于栈先进后出,B必须在C上面 
    int c[4],sta[Maxn],top=0,re=0,now=1;c[1]=A;c[2]=B;c[3]=C;
    bool can;//can=true表示栈中可以放C 
    for(int i=1;i<=n;i++)
    {
        while(top&&sta[top]==now)top--,re++;//能处理栈中垃圾则处理 
        if(!top||sta[top]==3)can=true;
        else can=false;
        //更新现在是否能放C 
        if(h[c[now]][b[i]]){re++;continue;}//能直接处理则处理 
        else//now处理不了 
        {
            if(now==3)break;//ABC都处理不了 
            if(!top)//栈为空 
            {
                if(h[C][b[i]])sta[++top]=3;//可以放C就放C,贪心的思想,因为放了B就不能放C 
                else if(h[B][b[i]])sta[++top]=2;//否则放B 
                else return re+top;//否则这个垃圾ABC都处理不了,只能把栈中的全处理了 
            }
            else
            {
                if(h[C][b[i]]&&can)sta[++top]=3;//可以放C就放C,贪心的思想,因为放了B就不能放C 
                else if(h[C][b[i]]&&!can)//此时now=1,想想为什么 
                {
                    if(!h[B][b[i]])
                    {//B不能处理,而C能处理,因为不能把C放在B上,所以要转为B,先把栈中的B处理了,再把C放入栈中 
                        now++;
                        while(top&&sta[top]==now)top--,re++;
                        sta[++top]=3;
                    }
                    else sta[++top]=2;//B能够处理就把B放入栈中 
                }
                else
                {
                    if(!h[B][b[i]])return re+top;
                    //ABC都处理不了,必须转成C,由于栈中的都是能够处理的,所以直接可以把栈中的全部处理 
                    sta[++top]=2;//B能够处理就把B放入栈中 
                }
            }
        }
    }
    return re;
}
int main()
{
    n=read(),K=read(),S=read();
    for(int i=1;i<=S;i++)
    {
        int x=read();
        while(x)
        {
            h[i][x]=true;
            a[x][++len[x]]=i;
            x=read();
        }
    }
    for(int i=1;i<=n;i++)b[i]=read();
    for(int i=1;i<=len[b[1]];i++)
    {
        int A=a[b[1]][i],pos=2;
        for(;pos<=n;pos++)if(!h[A][b[pos]])break;
        if(pos==n+1&&h[A][b[n]]){printf("%d\n%d 0 0",n,A);return 0;}//判断特殊情况 
        int t1=pos;
        for(int j=1;j<=len[b[t1]];j++)
        {
            int B=a[b[t1]][j];pos=t1;
            for(;pos<=n;pos++)if(!h[A][b[pos]]&&!h[B][b[pos]])break;
            if(pos==n+1&&(h[A][b[n]]||h[B][b[n]])){printf("%d\n%d %d 0",n,A,B);return 0;}//判断特殊情况 
            for(int k=1;k<=len[b[pos]];k++)
            {
                int C=a[b[pos]][k];
                //枚举ABC及其顺序 
                int t1=work(A,B,C),t2=work(A,C,B),t3=work(B,A,C);
                int t4=work(B,C,A),t5=work(C,A,B),t6=work(C,B,A);
                if(t1>mx)mx=t1,ansA=A,ansB=B,ansC=C;
                if(t2>mx)mx=t2,ansA=A,ansB=C,ansC=B;
                if(t3>mx)mx=t3,ansA=B,ansB=A,ansC=C;
                if(t4>mx)mx=t4,ansA=B,ansB=C,ansC=A;
                if(t5>mx)mx=t5,ansA=C,ansB=A,ansC=B;
                if(t6>mx)mx=t6,ansA=C,ansB=B,ansC=A;
            }
        }
    }
    printf("%d\n%d %d %d",mx,ansA,ansB,ansC);
}

jobs

答案显然满足二分性质,那么我们就二分答案,判断的话就贪心判断。若当天能处理就当天处理,否则留到之后,每天优先处理更紧急的工作。时间复杂度为O(mlogm)。似乎有O(nlogm)的做法?我太菜了,不会,不写了。

代码:

#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
#define LL long long
#define pa pair<int,int> 
const int Maxm=1000010;
const int Maxn=100010;
int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return x*f;
}
int n,m,d,a[Maxm];
vector<int>b[Maxn];
bool check(int x,int op)//op为0则不输出方案,op为1则输出方案 
{
    queue<pa >q;//用一个队列存储之前天留的工作,队头的最紧急
    //first表示编号,second表示哪天接到任务 
    for(int i=1;i<=n;i++)//逐天处理 
    {
        int t=x;//今天有t台机器 
        while(!q.empty()&&t)
        {
            pa temp=q.front();
            if(temp.second+d<i)return false;//若这个工作已经过时,则方案不合法 
            if(op==1)printf("%d ",temp.first);
            q.pop();t--;
        }
        if(!q.empty()&&q.front().second+d<i)return false;//若这个工作已经过时,则方案不合法
        for(int j=0;j<b[i].size();j++)//处理今天的工作 
        if(t){t--;if(op==1)printf("%d ",b[i][j]);}
        else q.push(make_pair(b[i][j],i));
        if(op==1)puts("0");
    }
    return true;
}
int main()
{
    n=read();d=read();m=read();
    for(int i=1;i<=m;i++)a[i]=read(),b[a[i]].push_back(i);
    int l=1,r=m;
    while(l<=r)//二分答案 
    {
        int mid=l+r>>1;
        if(check(mid,0))r=mid-1;
        else l=mid+1;
    }
    printf("%d\n",r+1);
    check(r+1,1);//输出方案 
}

Sailing Race

问题分为两类,我们就先探讨k=0这种比较简单的情况。我们可以用动态规划解决。令f1[i][j]为以i作为起点,逆时针方向在[i,j)这个区间中走的最大边数,f2[i][j]为以i作为起点,顺时针方向在[i,j)这个区间中走的最大边数,然后就可以有n^3的方法转移了,具体来说,求f[i][x]可以通过枚举中转站y来转移,也就是说先由i走到y,再从y走到x。
对于第二类问题,考虑相交的是a->b,c->d这条路径,并且a是起点,那么其中一种可行解就是1(从a走到b)+b走单调方向(逆时针或者顺时针)到达c的最大边数(第一部分)+1(从c走到d)+MAX{[d,a)的最大边数,[d,b)的最大边数}(第二部分),其中第二部分已经在解决问题一的过程中预处理了,实际上,第一部分也可以在问题一中预处理,具体转移见代码。但是这样的转移还是n^4的,因为要枚举abcd四个点。但是我们又注意到,对于一个固定的b、c,a的位置是确定的,因为第一部分与a无关,而对于第二部分,a离c越近,值就越大,所以我们可以枚举c,b,预处理出a的最佳位置,然后枚举d,就可以做到n^3的转移。

代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define LL long long
const int Maxn=510;
int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return x*f;
}
/*为了方便,程序中点的编号是从0~n-1的*/
/*[x,y)表示包含x不包含y*/
int a[Maxn][Maxn],aa[Maxn][Maxn];//a[i][j]表示与i相连的第j个点
int b[Maxn],bb[Maxn];//b[i]表示有多少个点与i相连 
//aa和bb的功能分别与a和b类似,只不过a存储的是原图的边,aa存储的是原图的反向边
int f1[Maxn][Maxn],f2[Maxn][Maxn];
int f3[Maxn][Maxn],f4[Maxn][Maxn];
//f1[i][j]表示以i作为起点,逆时针方向在[i,j)这个区间中走的最大边数
//f2[i][j]的意义类似,表示顺时针方向 
//f3[i][j]以i作为起点,逆时针方向走到j的最大边数
//f4[i][j]以i作为起点,顺时针方向走到j的最大边数
bool map[Maxn][Maxn];//map[i][j]=true代表i到j有边
int start[Maxn];//start[x]表示x在当前i的最优起点 
int n,kk,Max,Maxi;
void calc()
{
    for(int t=1;t<=n;t++)//通过枚举i与x的距离t来枚举x 
    for(int i=0;i<n;i++)//枚举起点i 
    for(int j=1;j<=b[i];j++)//枚举与i相连的点y 
    {
        int y=a[i][j];//通过y来转移 
        int x=(i+t)%n,h=(y+n-i)%n;//i从逆时针方向走t步到达x,走h步到达y 
        if(h<t)//判断y是否在[i,x)范围内 
        {
            if(f3[y][x])f3[i][x]=max(f3[y][x]+1,f3[i][x]);
            f1[i][x]=max(max(f1[y][x]+1,f2[y][i]+1),f1[i][x]);
        }
        x=(i+n-t)%n;h=(i+n-y)%n;//i从顺时针方向走t步到达x,走h步到达y
        if(h<t)//判断y是否在[i,x)范围内 
        {
            if(f4[y][x])f4[i][x]=max(f4[y][x]+1,f4[i][x]);
            f2[i][x]=max(max(f2[y][x]+1,f1[y][i]+1),f2[i][x]);
        }
    }
    Max=0;
    for(int i=0;i<n;i++)if(f2[i][i]>Max)Max=f2[i][i],Maxi=i;
    for(int i=0;i<n;i++)if(f1[i][i]>Max)Max=f1[i][i],Maxi=i;
    //记录答案 
}
void work()
{
    int tmp;
    for(int i=0;i<n;i++)
    {
        for(int j=2;j<n;j++)//找x目前的最优起点 
        {
            int x=(i+j)%n,k=1;
            while(k<=bb[x]&&(aa[x][k]+n-i-1)%n>=j)k++;
            if(k<=bb[x])start[x]=aa[x][k];
            else start[x]=-1;
        }
        for(int j=2;j<=n-2;j++)//枚举z 
        if(map[i][(i+j)%n])
        {
            int z=(i+j)%n;
            for(int k=j+1;k<n;k++)//枚举x 
            {
                int x=(i+k)%n;
                if(start[x]!=-1&&(n+start[x]-i)%n<j&&f3[x][i])
                //3个条件:1、有起点 2、起点在i~z之间 3、x~i至少有1条边 
                {
                    tmp=2+f3[x][i]+max(f1[z][x],f2[z][start[x]]);
                    if(tmp>Max)Max=tmp,Maxi=start[x];
                }
            }
        }
        //下面与上面类似,在此就不再赘述了 
        for(int j=1;j<=n-2;j++)
        {
            int x=(i+j)%n,k=1;
            while(k<=bb[x]&&(aa[x][k]+n-i)%n>j)k++;
            if(k>1)start[x]=aa[x][k-1];
            else start[x]=-1;
        }
        for(int j=2;j<=n-2;j++)
        if(map[i][(i+j)%n])
        {
            int z=(i+j)%n;
            for(int k=1;k<j;k++)
            {
                int x=(i+k)%n;
                if(start[x]!=-1&&(n+start[x]-i)%n>j&&f4[x][i])
                {
                    tmp=2+f4[x][i]+max(f1[z][start[x]],f2[z][x]);
                    if(tmp>Max)Max=tmp,Maxi=start[x];
                }
            }
        }
    }
}
int main()
{
    n=read();kk=read();
    for(int i=0;i<n;i++)
    {
        int x=read();
        while(x)
        {
            map[i][x-1]=true;
            x=read();
        }
    }
    for(int i=0;i<n;i++)//按逆时针顺序存储边
    {
        int j=(i+1)%n;
        while(i!=j)
        {
            if(map[i][j])
            {
                a[i][++b[i]]=j;
                f3[i][j]=f4[i][j]=1;
            }
            if(map[j][i])aa[i][++bb[i]]=j;
            j++;if(j==n)j=0;
        }
    }
    calc();
    if(kk==1)work();
    printf("%d\n%d",Max,Maxi+1);
}

highway(交互题)

先询问123。若123在同一直线上,那就从4开始,问(1,i,i+1)。若(1,i,i+1)在同一直线上,继续问下一个i;否则,问(1,2,i),那么就可以得知i和i+1哪个与123不在同一直线上;若123不在同一直线上,那就问456。若不在同一直线上,那么123456这6个点就能确定答案,大力分类讨论即可;若456在同一直线上,那么类似123在同一直线上的情况,注意细节即可。代码没有好好测,只自己随便测了测,不敢保证正确性,若有任何问题欢迎指出。

代码:

#include"office.h"
int main()
{
    int n=GetN(),cnt=0,a[5];
    if(isOnLine(1,2,3)==1)
    {
        for(int i=4;i<n;i+=2)
        if(isOnLine(1,i,i+1)==0)
        {
            if(isOnLine(1,2,i)==1)a[++cnt]=i+1;
            else a[++cnt]=i;
            if(cnt==2)break;
        }
        if(cnt==1)a[++cnt]=n;
        Answer(1,2,a[1],a[2]);
    }
    else
    {
        if(isOnLine(4,5,6)==0)
        {
            if(isOnLine(1,2,4)==1)
            {
                if(isOnLine(1,2,5)==1)Answer(1,2,3,6);
                else Answer(1,2,3,5);
            }
            else if(isOnLine(1,2,5)==1||isOnLine(1,2,6)==1)Answer(1,2,3,4);
            /****************************************/
            else if(isOnLine(1,3,4)==1)
            {
                if(isOnLine(1,3,5)==1)Answer(1,3,2,6);
                else Answer(1,3,2,5);
            }
            else if(isOnLine(1,3,5)==1||isOnLine(1,3,6)==1)Answer(1,3,2,4);
            /****************************************/
            else if(isOnLine(2,3,4)==1)
            {
                if(isOnLine(2,3,5))Answer(2,3,1,6);
                else Answer(2,3,1,5);
            }
            else Answer(2,3,1,4);
        }
        else
        {
            for(int i=7;i<n;i+=2)
            if(isOnLine(4,i,i+1)==0)
            {
                if(isOnLine(4,5,i)==1)a[++cnt]=i+1;
                else a[++cnt]=i;
                if(cnt==2)break;
            }
            if(cnt==2)Answer(4,5,a[1],a[2]);
            else
            {
                if(cnt==0)Answer(4,5,1,2);
                else
                {
                    if(isOnLine(1,4,5)==0)Answer(4,5,a[1],1);
                    else if(isOnLine(2,4,5)==0)Answer(4,5,a[1],2);
                    else Answer(4,5,a[1],3);
                }
            }
        }
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值