noip 2017题解

一、小凯的疑惑:

去年比赛时懵逼报零的题目,考完之后大家告诉我是小学奥数,直接ab-a-b就A了。之后去向别人请教,某些人说:这题就是一道打表题,直接打表就行了,或者看洛谷的题解上面大多都是在证明为什么ab-a-b是答案是正确的,可是我考虑的是,这个ab-a-b从何而来?题解大多数好像并没有提到,或者有的dalao写的exgcd我也不清楚是怎么求的,机房的学长去年有写exgcd的但是挂掉了。 今天,借助某位dalao的思路,我来为大家提供一种新的简单的思路(毕竟这是noip的第一题,不会那么难吧?)a与b是互质的,我们可以无限使用a和b来组成一些数,我们要求的是最大的那个不能有a和b组成的数。假设只有b,那么b可以组成的数一定是0,b,2b……那么a和b组合起来的作用在哪儿呢?学过同余的都知道,只使用b的时候,我们关于模b的剩余系中就只枚举出来了余数为0的情况。a的作用就是把它乘上某一个倍数,使它出现模b的另一个剩余系。我们发现,因为a,b互质,只要把a乘上b-1次,此时就已经出现了所有的剩余系。这时候到当前这个数,我们其实已经覆盖了模b的所有剩余系,所以它之后的所有数都可以由a,b组成。而对于之前的数:到a(b-1)这个数的时候我们刚好覆盖了所有的剩余系,设a(b-1)%b=r,那么说明在这之前模b余数为r的值是无法组成的,而从它到a(b-1)这中间的剩余系因为在之前已经被覆盖了,所以都可以组成。那么最大的不能组成的数就是a(b-1)-b,也就是a*b-a-b。

二、奶酪:

  貌似思路很多?有人写的并查集,有人写的最短路好像,我是直接建图然后跑dfs。noip的时候也是这么写的,具体怎么挂成40了我也记不清楚了,总之是送分题。

#include<bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int maxn=1e3+10;
int n,t,h,r,tot;bool flag;
int ver[maxn*maxn],Next[maxn*maxn],lin[maxn];
int x[maxn],y[maxn],z[maxn];bool v[maxn];
void add(int x,int y)
{
    ver[++tot]=y;Next[tot]=lin[x];lin[x]=tot;
}
void dfs(int x)
{
    if(x==n+1){
    flag=1;return;}
    for(int i=lin[x];i;i=Next[i]){
        int y=ver[i];
        if(!v[y]){
            v[y]=1; dfs(y);
        }
    }
    return ;
}
int main()
{
    scanf("%d",&t);
    while(t--){
        memset(Next,0,sizeof(ver));
        memset(lin,0,sizeof(lin));
        scanf("%d%d%d",&n,&h,&r);
        for(int i=1;i<=n;i++){
            scanf("%d%d%d",&x[i],&y[i],&z[i]);
        }
        for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++){
            if(2*r>=sqrt(1.0*(x[i]-x[j])*(x[i]-x[j])+1.0*(y[i]-y[j])*(y[i]-y[j])+1.0*(z[i]-z[j])*(z[i]-z[j])))
            add(i,j),add(j,i);
        }
        for(int i=1;i<=n;i++) if(r>=z[i]) add(0,i),add(i,0);
        for(int i=1;i<=n;i++) if(z[i]+r>=h) add(i,n+1),add(n+1,i);
        memset(v,0,sizeof(v));
        v[0]=1;flag=0;
        dfs(0);
        if(flag) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

三、逛公园

  设1到N的最短路为d,题目让求的就是,关于给定的图,从1到N路径长度不大于d+k的路径数。然后题目中可以存在零环。如果有无数解,就输出-1。

  简单分析一下,我们要做的有两件事,一个是判断是否存在零环并且计算会不会对答案做出贡献,如果是,那么一定有无数组解,反之则零环对答案没有影响。 第二件事就是求路径数了。

  30分暴力是k=0,并且没有零环,那我们只需要做最短路计数就行。

  70分是没有零环的情况,我们考虑如何求解。其实看了一下数据,k不大,我们可以在图上做dp。设f【x】【i】表示从1走到x,路径长度为d【x】+i的方案数。每一个状态只能由它相邻的点更新,然后写递推式或者记忆化搜索就可以了。

  100分的写法:其实我们可以第一件事就做零环,我们把所有的零环找出来,然后用并查集把每一个零环上的点合并起来成一个点,判断1到这个点的最短距离加上这个点到N的最短距离,是否小于等于d+k,如果有一个点符合条件,就输出-1.如果所有的都不符合,说明不存在无数解,就可以再开始做dp就可以了。当然其实如果你写的dp是记忆化搜索,不用写并查集那么麻烦。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int M=2e5+10;
int t,n,m,k,p,tot,ver[M],Next[M],lin[N],edge[M],d[N],v[N],vc[M],Nex[M],lc[N];
int f[N][60],flag[N][60];
void add(int x,int y,int z){
    ver[++tot]=y;Next[tot]=lin[x];lin[x]=tot;edge[tot]=z;
    vc[tot]=x;Nex[tot]=lc[y];lc[y]=tot;
}
void dijkstra(){
    priority_queue<pair<int,int > >q;
    memset(d,0x3f,sizeof(d));
    d[1]=0;q.push(make_pair(0,1));
    while(q.size()){
        int x=q.top().second;q.pop();
        if(v[x]) continue;
        v[x]=1;
        for(int i=lin[x];i;i=Next[i]){
            int y=ver[i];
            if(d[y]>d[x]+edge[i]){
                d[y]=d[x]+edge[i];
                q.push(make_pair(-d[y],y));
            }
        }
    }
}
int dfs(int x,int l){
    if(l<0||l>k) return 0;
    if(flag[x][l]){
        flag[x][l]=0;
        return -1;
    }
    if(f[x][l]!=-1) return f[x][l];
    int ans=0;
    flag[x][l]=1;
    for(int i=lc[x];i;i=Nex[i]){
        int y=vc[i];
        int val=dfs(y,d[x]+l-edge[i]-d[y]);
        if(val==-1){
            flag[x][l]=0;
            return -1;
        }
        ans=(long long)(ans+val)%p;
    }
    flag[x][l]=0;
    if(x==1&&l==0) ans++;
    f[x][l]=ans;
    return ans;
}
int solve(){
    dijkstra();
    int ans=0;
    for(int i=0;i<=k;i++){
        int val=dfs(n,i);
        if(val==-1) return -1;
        ans=(long long)(ans+val)%p;
    }
    return ans;
}
int main(){
    scanf("%d",&t);
    while(t--){
        tot=0;
        scanf("%d%d%d%d",&n,&m,&k,&p);
        for(int i=1;i<=n;i++) lin[i]=lc[i]=v[i]=0;
        for(int i=1;i<=m;i++) Nex[i]=Next[i]=edge[i]=0;
        memset(f,-1,sizeof(f));
        memset(flag,0,sizeof(flag));
        for(int i=1;i<=m;i++){
            int x,y,z;scanf("%d%d%d",&x,&y,&z);
            add(x,y,z);
        }
        printf("%d\n",solve());
    }
}

四、时间复杂度

    这道题就是栈的纯模拟,就是处理的时候麻烦了一些。

    我们发现,在不考虑ERR的情况时,所有的结构都是循环,无非有嵌套,嵌套里还会有并列的,纯模拟计算时间复杂度比较麻烦,但是我们发现每次遇到一个E我们肯定要计算之前那个F的复杂度然后看它对答案的贡献的,一进一出,所以我们要用栈。

    每次遇到一次F,就算一下它的复杂度,然后把它入栈。如果遇到E,就把当前栈顶出栈,并且拿它来更新新的栈顶。按照这种思路写,你每次出栈之后新的栈顶一定是刚出栈的那个循环上面套的循环,所以更新答案时应该是两个复杂度相加。

    大致思路就是如此,具体细节自己考虑一下。

     其实你发现,ERR无非两种情况:

   (1)变量名重复定义 (2)开始循环的F与结束循环的E对不上

第二种情况在做栈的时候就可以判断,如果当前top0,但仍然要出栈,就输出ERR

第一种情况可以在记录栈的同时,记录一下变量名,用map处理。每次入栈的时候,判断变量名是否使用,如果使用了就输出ERR,否则就把变量名加入map,并且记录一下栈中每个元素对应的变量名,在出栈的时候把map中的变量名再删去。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map> 
using namespace std;
int t,l,m,top,say,a[210],b[210];
string ch[200];
map<string,bool> mp;
bool flag,tt;
int main(){
    //freopen("a.in","r",stdin);
    scanf("%d",&t);
    while(t--){
        int ans=0;
        memset(a,0,sizeof(a));
        memset(b,0,sizeof(b));
        mp.clear();
        top=0;
        scanf("%d",&l);
        char s[210];scanf("%s",s+1);
        m=strlen(s+1);//cout<<m<<endl;
        //for(int i=1;i<=m;i++) cout<<s[i];
        //cout<<endl;
        flag=0;say=0;tt=0; //flag是判断有没有n,say是n的幂 ,tt是看有没有输出过 
        for(int i=1;i<=m;i++){
            if(s[i]=='n') flag=1;
            if(s[i]>='0'&&s[i]<='9') say=say*10+s[i]-'0';
        }
        while(l--){
            string s1,s2,s3,s4;
            int len1=0,len2=0,num1=0,num2=0;bool n1=0,n2=0;
            cin>>s1;// cout<<s1<<' ';
            if(s1=="F"){
                cin>>s2>>s3>>s4; //cout<<s2<<' '<<s3<<' '<<s4<<endl;
                if(mp[s2]){
                    tt=1;
                }
                mp[s2]=1;
                len1=s3.size();len2=s4.size();
                for(int i=0;i<len1;i++){
                    if(s3[i]=='n') n1=1;
                    if(s3[i]>='0'&&s3[i]<='9') num1=num1*10+s3[i]-'0';
                }
                for(int i=0;i<len2;i++){
                    if(s4[i]=='n') n2=1;
                    if(s4[i]>='0'&&s4[i]<='9') num2=num2*10+s4[i]-'0';
                }
                top++;
                //cout<<"n1:"<<n1<<" "<<"n2:"<<n2<<' '<<"num1:"<<num1<<" num2:"<<num2<<endl;
                if(n1&&n2) a[top]=b[top]=0;
                else if(n1&&!n2||(num1>num2&&!n1&&!n2)) a[top]=b[top]=-1;
                else if(!n1&&n2) a[top]=b[top]=1;
                else if(num1<=num2) a[top]=b[top]=0;
                //for(int i=1;i<=top;i++) cout<<a[top]<<' '<<b[top]<<endl;
                ch[top]=s2;
            }else{
                if(top==0){
                    tt=1;continue;
                }
                if(a[top-1]!=-1){
                    //cout<<"a[top-1]:"<<a[top-1]<<' '<<"b[top]:"<<b[top]<<endl;
                    b[top-1]=max(b[top-1],a[top-1]+max(b[top],0));
                    //cout<<"b[top-1]:"<<b[top-1]<<' '<<"top-1:"<<top-1<<endl;
                }
                mp[ch[top]]=0;
                a[top]=b[top]=0;
                top--;
                if(top==0) ans=max(ans,b[0]);
            }
        }//cout<<top<<' '<<b[0]<<' '<<flag<<' '<<say<<endl;
        if(tt) printf("ERR\n");
        else if(top>0) printf("ERR\n");
        else if(b[0]==0&&flag==0) printf("Yes\n");
        else if(flag&&ans==say) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

   五、宝藏

     n<=12,显然是搜索或者状压。我们可以枚举每一个点作为起点,然后dfs,递归传递的是每一个二进制压缩的状态,再枚举每一个点作为当前所在点,然后再枚举要到达的点,更新最优值。稍微做两个剪枝,跑的很快。(也可能是数据水?)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 2147483647
int n,m,g[20][20],f[10000],dis[20],ans=INF;
void find(int x)
{
    if(f[x]>ans) return ;
    for(int i=1;i<=n;i++)
    if((1<<(i-1))&x)
    {
        for(int j=1;j<=n;j++)
        if(((1<<(j-1))&x)==0&&g[i][j]!=INF)
        {
            if(f[x|(1<<(j-1))]>f[x]+dis[i]*g[i][j])
            {
                int tmp=dis[j];
                dis[j]=dis[i]+1;
                f[x|(1<<(j-1))]=f[x]+dis[i]*g[i][j];
                find(x|(1<<(j-1)));
                dis[j]=tmp;
            }
        } 
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    g[i][j]=INF;
    int u,v,z;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u,&v,&z);
        g[u][v]=min(g[u][v],z);
        g[v][u]=min(g[v][u],z);
    }
    for(int o=1;o<=n;o++)
    {
        for(int i=1;i<=n;i++) dis[i]=INF;
        for(int i=1;i<=(1<<n)-1;i++) f[i]=INF;
        dis[o]=1;
        f[1<<(o-1)]=0;
        find(1<<(o-1));
        ans=min(ans,f[(1<<n)-1]);
    } 
    printf("%d",ans);
    return 0;
}

六、列队

     我们发现,每一次出去一个人之后,改变的位置只有当前所在行,以及最后一列。考虑我们其实每次修改一次,可以把原来的那个点删除,然后分别在行、列的末尾新加入一个点,然后每次查询我们要做的是求区间第k个数。这个可以用线段树实现。我们开n+1棵线段树,前n个分别维护的是矩阵的n行的前m-1的人编号的情况,第n+1棵维护的是最后一列的人的编号情况。每次要求x,y位置人的编号,如果y=m,我们就是在第n+1棵线段树上找第x个数的val。否则就是在第x棵线段树上找第y个数的val;修改类似。当然,开n+1棵线段树,复杂度肯定爆炸啊,所以我们要动态开点。首先,线段树维护的区间长度是多少呢?我们发现,初始长度为max(n,m),最多加了q个点,那么最大长度p就是max(n,m)+q;一般动态开点,线段树的空间要p*20.不过已经符合了本题的要求。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=3e5+10;
int n,m,q,p,tot,now;
int ls[maxn*20],rs[maxn*20],root[maxn],siz[maxn*20],pos[maxn];
ll val[maxn*20];
int read(){
    char ch=getchar();int num=0,f=1;
    while(!isdigit(ch)){if(ch=='-') f=-1; ch=getchar();}
    while(isdigit(ch)){num=num*10+ch-'0'; ch=getchar();}
    return num*f;
}
void init(){
    n=read();m=read();q=read();
}
int calc(int l,int r){//计算这一段区间初始有多少个数
    if(now==n+1){
        if(r<=n) return r-l+1;
        else if(l<=n) return n-l+1;
        else return 0;
    }
    if(r<m) return r-l+1;
    else if(l<m) return m-l;
    else return 0;
}
ll query(int &id,int l,int r,int x){
    if(!id){//若当前节点没有开
        id=++tot;//新开一个节点
        siz[id]=calc(l,r);//计算当前节点的数的个数
        if(l==r) {//如果是叶子节点
            if(now<=n) val[id]= (ll)m*(now-1)+1LL*l;
            else val[id]= (ll)l*m;//计算节点编号
        }
    }
    siz[id]--;//将一个点去除
    if(l==r) return val[id];
    int mid=l+r>>1;
    if(!ls[id]&&mid-l+1>=x) return query(ls[id],l,mid,x);
    if(ls[id]&&siz[ls[id]]>=x) return query(ls[id],l,mid,x);
    if(ls[id]) x-=siz[ls[id]];
    else x-=mid-l+1;
    return query(rs[id],mid+1,r,x);
} 
void update(int &id,int l,int r,ll temp,int x){
    if(!id){
        id=++tot;
        siz[id]=calc(l,r);
        if(l==r){
            val[id]=temp;
        }
    }
    siz[id]++;//新增一个节点
    if(l==r) return ;
    int mid=l+r>>1;
    if(mid>=x) update(ls[id],l,mid,temp,x);
    else update(rs[id],mid+1,r,temp,x);
}
void work(){
    ll ans;p=max(m,n)+q;
    for(int i=1;i<=q;i++){
        int x,y;scanf("%d%d",&x,&y);
        now=x;
        if(y==m){
            now=n+1;
            ans=query(root[now],1,p,x);
        }else
            ans=query(root[now],1,p,y);
        printf("%lld\n",ans);
        now=n+1;pos[now]++;
        update(root[now],1,p,ans,n+pos[now]);
        if(y^m){
            ans=query(root[now],1,p,x);
            now=x;++pos[now];
            update(root[now],1,p,ans,m-1+pos[now]);
        }
    }
}
int main(){
    init();	
    work();
    return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值