1. 程式人生 > >noip 2017題解

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;
}