1. 程式人生 > >【Contest】Nowcoder 假日團隊賽1 題解+賽後總結

【Contest】Nowcoder 假日團隊賽1 題解+賽後總結

比賽連結

通過順序:\(B\rightarrow D\rightarrow I\rightarrow J\rightarrow G\rightarrow H \rightarrow A \rightarrow K\rightarrow C\rightarrow E \rightarrow L \rightarrow F\)

A 蹄球錦標賽:

給你\(N\)個點\(N\)條有向邊,求在某些點開始跑邊能夠遍歷整張圖的最少點數

這已經是簡化版的題意了,略去了前面沒必要講的建邊(因為對於一個能看懂並寫出AC程式碼的大佬來說這不是啥問題),建邊唯一的坑點就是任意一頭牛的左右兩頭牛距離相同連向左邊(原題面括號那一句寫錯了)。

具體講後面的操作:

如果一個點\(x\)連向\(y\)一條有向邊,那麼顯然放在\(x\)比放在\(y\)更優,因為放\(x\)一定到\(y\),放\(y\)不一定能到\(x\)。

我們不妨做出一個假設:只有沒有有向邊連到的點是需要放球的

這已經非常接近正解了,但是很可惜,少考慮了一種情況

我們考慮這組資料

4
1 3 1 4

這樣子相當於構造出了兩個大小為二的環(事實上本題最大環大小就是二,但這不是重點,所以讀者可以自證)

這樣子顯然要輸出\(2\),但是我們剛剛那樣寫會輸出\(0\)

我們意識到應該把所有的環消除掉(比如縮成一個點)

所以我們使用tarjan縮點(可以參考洛谷或者其他OJ的題目,這裡不過多說明)將圖變為DAG(有向無環圖)

然後統計一下有多少入度為\(0\)的點即可

時間複雜度:建邊\(O(n log n)\),縮點\(O(n)\)

總時間\(O(n log n)\)

\(ACcode:by\space WAduck\)(可能有多餘變數或陣列,直接無視就好了)

#include<iostream>
#include<queue>
#include<algorithm>
#define maxn 100001
#define t edge[i].to
#define s edge[i].nex
using namespace std;
struct Edge
{
    int to,nex;
}edge[maxn],edg[maxn];
int st[maxn],dis[maxn],DFN[maxn],LOW[maxn],dye[maxn],vis[maxn],size[maxn],stack[maxn],sta[maxn],degree[maxn],dist[maxn],x1[maxn],y2[maxn];
int n,m,cnt,stacksize,color,dfsnum,c,a[2001],maxx;
void addedge(int x,int y)
{
    edge[++cnt].to=y;
    edge[cnt].nex=st[x];
    st[x]=cnt;
}
void tarjan(int start)
{
    vis[stack[++stacksize]=start]=1;
    LOW[start]=DFN[start]=++dfsnum;
    for(int i=st[start];i;i=s)
    if(!DFN[t])tarjan(t),LOW[start]=min(LOW[start],LOW[t]);
    else if(vis[t])LOW[start]=min(LOW[start],DFN[t]);
    if(DFN[start]==LOW[start])
    {
        int y;
        while(y=stack[stacksize--])
        {
            dye[y]=start;
            vis[y]=0;
            if(start==y)break;
            dis[start]+=dis[y];
        }
    }
}
int main()
{
    cin>>n;
    m=n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    sort(a+1,a+1+n);
    addedge(1,2);
    x1[1]=1;y2[1]=2;
    x1[n]=n;y2[n]=n-1;
    addedge(n,n-1);
    for(int i=2;i<n;i++)
    {
        if(a[i]-a[i-1]<=a[i+1]-a[i])addedge(i,i-1),x1[i]=i,y2[i]=i-1;
        else addedge(i,i+1),x1[i]=i,y2[i]=i+1;
    }
    for(int i=1;i<=n;i++)if(!DFN[i])tarjan(i);
    for(int i=1;i<=m;i++)
    {
        int x=dye[x1[i]],y=dye[y2[i]];
        maxx=max(maxx,max(x,y));
        //cout<<x1[i]<<' '<<y1[i]<<' '<<x<<' '<<y<<endl;
        if(x!=y)
        {
            edg[++c].to=y;
            edg[c].nex=sta[x];
            sta[x]=c;
            degree[y]++;
        }
    }
    int ans=0;
    for(int i=1;i<=maxx;i++)
    {
        //cout<<degree[i]<<endl;
        if(!degree[i]&&dye[i]==i)ans++;
    }
    cout<<ans;
    return 0;
}

B:便便傳送門(一)

(有味道的題)

這裡的傳送門可以雙向傳送,但是我們最多使用一次傳送門就夠了(顯而易見讀者自證)

我們要分三種情況討論

一,不使用傳送門

答案為兩點的直線距離\(\left|a-b\right|\)

二,使用傳送門從左邊到右邊

答案為起點到左側傳送門的直線距離加上右側傳送門到終點的直線距離

三,使用傳送門從右邊到左邊

與二同理

最後輸出三種情況的最小值即可

\(ACcode:by \space kan\_kiz\)

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
int main()
{
    int maxn=99999999;
    int a,b,x,y;
    scanf("%d%d%d%d",&a,&b,&x,&y);
    maxn=min(abs(x-a)+abs(y-b),min(abs(x-b)+abs(y-a),abs(b-a)));
    printf("%d",maxn);
    return 0;
}

C: 便便傳送門(二)

根據上一道題的公式,我們能很快的概括題面:

選取一個值\(x\),使\(\sum^{n}_{i=1}min(\left| a[i]-b[i]\right|,\left|a[i]\right|+\left|b[i]-x\right|)\)的值最小

解法一:(瞎搞,期望得分:\(20\))

我們發現最優的\(x\)會出現在一段(或多段)區間中,並且在一定範圍內滿足單調性,我們可以使用模擬退火(什麼鬼),但是顯然最優答案區間可以構造長度為\(1\),而且ACM賽制就別想著騙分了

正解:

其實想到正解是很意外的,首先是\(yxy\)打聽到是什麼斜率(她說的是斜率優化)

然後那天我又無所事事地在玩函式影象生成器

然後我想到了把式子帶進去

結果是很驚人的

有一些函式成了常函式,有一些則是線性分段函式

分段函式的交點還是整點,我當時就明白了怎麼寫2333

首先我們要求函式的交點座標

一,首先我們討論什麼時候是常函式

答案是顯然的,走到傳送門的距離比直接到終點的距離還長,那麼縱座標\(y= \left|a-b\right|\)

二,由於考慮到傳送門是單向傳送的,所以我們還有兩種情況需要討論

對於第一種情況,\(a\)去\(b\)的路上經過傳送門入口(也可以說是\(a,b\)兩點異側),所以條件是

\(\begin{cases}a<0 \space and\space a<b\\ or\space a>=0\space and \space a>=b\end{cases}\)

(其實就是\(a,b-a\)同號或者說\(a,b\)異號)

對此讀者可能有個疑問,為什麼不會出現\(a,b\)在原點同一邊的情況呢

其實是會的,但是會歸為我們剛剛討論的常函式(因為\(a\)到傳送門起點的路上一定經過\(b\))

然後我們來討論一下座標

首先想想什麼時候\(y=\left | a-b\right |\)

首先如果傳送門是從零傳送到,那麼顯然要走完全部的路程

所以第一個斜率改變時的座標是\((0,\left | a-b\right |)\)

那麼什麼時候\(y\)值最小呢

顯然傳送門是從零到\(b\)的,這樣子我們就只需要走\(\left | a \right |\)的路程(用於走到傳送門起點)

我們得出第二個座標\((b,\left | a \right |)\)

然後這個函式影象是對稱的,我們可以通過前兩個推出第三個\((2b,\left | a-b\right |)\)

三,剛剛是經過傳送門起點的,那麼現在就是相反

讀者有興趣可以自己推,為了節省篇幅,在此直接給出座標

\((2a,\left | a-b\right |),(b,\left | a \right |),(2b-2a,\left | a-b\right |)\)

知道了座標,我們需要計算函式的斜率,從而統計\(y\)值

我們可以從最左邊開始,如果這段函式的起點\(x\)是\(z\),終點是\(c\),斜率是\(k\),那麼\(y\)增加\(k(c-z)\)

我們只要在每個改變的點那裡(最壞\(3n\)個),修改\(y\)值,統計最小值就可以了

不過需要注意,初始\(y\)值應該是\(\sum^{n}_{i=1}y[i]\)(這時所有函式是常函式)

由於區間\([-1e8,1e8]\),雖然可以用堆存,但是我還是偷懶用了\(map\)

小技巧:

如果定義了\(map\)的迭代器\(it\),那麼\(it->first\)可以返回\(key\),\(it->second\)可以返回\(value\)

\(ACcode:by \space WAduck\)

#include<iostream>
#include<map>
#include<cmath>
#include<cstdlib>
using namespace std;
#define int long long
map<int,int>m;
map<int,int>::iterator it;
int n,sum,ans=999999999,k;
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int a,b;
        cin>>a>>b;
        sum+=llabs(a-b);
        if(llabs(b-a)<=llabs(a))continue;
        if(a*b>=0)m[2*a]--,m[b]+=2,m[2*b-2*a]--;
        else m[0]--,m[b]+=2,m[2*b]--;
    }
    int last;
    for(it=m.begin();it!=m.end();it++)
    {
        sum+=k*((it->first)-last);
        last=it->first;
        k+=it->second;
        ans=min(ans,sum);
    }
    cout<<ans;
    return 0;
}

D: Promotion Counting

這顯然是反應\(USACO\)賽制的題啊

十萬個印度奶牛統計了一場\(USACO\)比賽,它們分別統計了賽前和賽後的銅組,銀組,金組,白金組的人數

要你求出有分別多少人從銅銀金組上升了一個等級

首先我們絕對不會從中間開始考慮,比如金組賽前賽後是\(1 \space \space 1\),那就沒有人上分嗎

不一定,有可能上來了\(x\)人,上去了\(x\)人

但是換在銅組或者白金組就不一樣了

所以我們可以選擇從銅組或者白金組開始遞推人數

但是最好是白金組

如果我是出題面和造資料的,我就說有人在比賽時剛剛註冊了

\(ACcode:by \space kan\_kiz\)

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
int a[7],b[7],ans[7];
int main()
{
    for (int i=1;i<=4;i++)
    {
        scanf("%d%d",&a[i],&b[i]);
    }
    ans[3]=b[4]-a[4];
    ans[2]=b[3]-a[3]+ans[3];
    ans[1]=b[2]-a[2]+ans[2];
    for (int i=1;i<=3;i++)
    {
        printf("%d\n",ans[i]);
    }
    return 0;
}

G:Superbull

給定一個集合\(s\)內的\(n\)個數,

進行一種操作:

從集合內選出兩個數進行異或(\(xor\)),異或的值計入貢獻,並且將兩個數中的一個數從集合中刪去。

重複\(N-1\)次此操作,直到集合內只剩下最後一個數。

求最大貢獻值。

資料範圍:\((1<=N<=2000)\)

首先我們不妨猜想一下貪心思路

選前\(N-1\)大的異或和作為答案

很可惜,這是錯誤的,因為沒有考慮不可行情況

什麼是不可行情況?

我們假設集合裡有\(\{a,b,c,d\}\),\(a \space xor\space b\),\(b \space xor\space c\),\(a \space xor\space c\)是前\(N-1\)大值

我們並不能選這三個組數作為答案

因為如果我們選\(a,b\)刪\(a\)

那麼\(a,c\)是不可能選到的

反之亦然

也就是說如果刪除的某些二元組構成了環,那麼這個方法不可行

\(N\)個點沒有環又有\(N-1\)條邊,顯而易見是生成樹,為了保證答案最大,只需要連邊跑最大生成樹即可

我們可以將每個數編號\(1 \to N\),結點兩兩連一條 邊權為【兩數異或值】的邊

然後對得到的圖跑一遍\(Kruskal\)或者\(Prim\)即可。

最後請注意,資料範圍較大,需要開\(long\space long\)

思路\(by \space WAduck\) \(ACcode:by\) \(Kan\_kiz\)

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define MAXN 2023
using namespace std;
struct qwq
{
    int u,v,w;
}e[4000023];
int w[MAXN];
int tot=0;
int f[MAXN];
int find(int x)
{
    if (f[x]==x) return x;
    return f[x]=find(f[x]);
}
bool cmp(const qwq x,const qwq y)
{
    return x.w>y.w;
}
int main()
{
    int n;
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%d",&w[i]);
    }
    for (int i=1;i<=n;i++)
    {
        f[i]=i;
    }
    int xx,yy;
    for (int i=1;i<n;i++)
    {
        for (int j=i+1;j<=n;j++)
        {
            e[++tot]=(qwq){ i,j,(w[i] xor w[j]) };
        }
    }
    long long ans=0;
    sort(e+1,e+tot+1,cmp);
    for (int i=1;i<=tot;i++)
    {
        xx=find(e[i].u); yy=find(e[i].v);
        if (xx!=yy)
        {
            ans+=e[i].w;
            f[xx]=yy;
        }
    }
    printf("%lld",ans);
    return 0;
}

K:大型聚會

給你一棵\(N\)個節點的樹,每次刪去一個葉子節點,直到剩下一個節點

但是有\(M\)個限制\(a,b\),表示如果\(a\)沒被刪除,\(b\)不能刪除

最後輸出所有能成為最後一個節點的節點

顯而易見,對於每個限制,以\(b\)為根時\(a\)的子樹(包括自己)都是不可行點

於是我們可以寫出一個支援子樹修改和換根資料結構,比如\(TopTree\),樹鏈剖分(參見\(LOJ139\))

但是單單這個結論是不夠的,我們觀察一下這組資料

3 2
1 2 
2 3
1 3
3 1
0
0
0

我們的程式會輸出

0
1
0

因為沒有考慮無解情況

那麼如何考慮無解情況呢

該題中的無解就是指沒有一個點是可行點,那麼我們其實只要找出任何一個可行點就可以了

怎麼找?

我們將樹上的\(n-1\)條邊連成雙向邊,將限制裡的\(a\)連向\(b\)一條單向邊

那麼,我們每次就只能刪除入度為\(1\)的點

因為如果這個點不是葉子節點,入度至少為\(2\),如果有被連有向邊(被約束),入度也至少為\(2\)

隨便找一個入度為\(1\)的點刪除,接下來將它連向的邊都刪除(也就是說連向的點入度--),然後如果更新後連向的點的入度為\(1\)(不需要每次\(O(n)\)統計,只統計這次刪除更新的點就行了),加入待定刪除點

最後,如果還沒有刪除\(n-1\)個點就無法繼續刪了(沒有入度為\(1\)的點了,比如每個點都被互相約束),說明無解,否則有解

這部分程式碼(為了方便第二種解法,我用了\(vector\)存(\(to\)是雙向邊,\(d\)是單向邊)):

queue<int>q;
int findroot()
{
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        if(!deg[x]/*入度*/)return x;
        for(int i=0;i<to[x].size();i++)
        {
            if(--deg[to[x][i]]==1)
            {
                q.push(to[x][i]);
            }
        }
        for(int i=0;i<d[x].size();i++)
        {
            if(--deg[d[x][i]]==1)
            {
                q.push(d[x][i]);
            }
        }
    }
    return 0;
}

這樣寫的話,時間複雜度為\(O(m\space log^2\space m)\)

程式碼:咕咕咕

但是我們剛剛那個方法顯然不夠優(\(hyf:\)太暴力了),雖然足以通過本題,但是如果資料加到\(3e6\),顯然過不了(吧,有鬆鬆鬆真傳我也沒辦法2333)

首先我們要證(kou)明(hu)兩個結論:

一,可行點連通

因為每次修改不可行點只是修改子樹,並不破壞可行點的連通性,所以成立

二,任意一個可行點作為根,刪除每個限制中的\(a\)的子樹,剩下的都是可行點

這個我真不會證,我的想法是將可行點看為樹的中間部分,不可行點是樹的一些邊角,從中間往四周邊角擴散不需要考慮從哪裡開始,由於可行點連通,所以不需要每次換根

我們將\(a\)的位置打上標記,以\(findroot\)的返回值(任意一個可行點)為起始點,遇到標記直接\(return\),最後輸出答案即可

時間複雜度:\(findroot\space O(n)\),\(dfs\space O(n)\)

總時間複雜度\(O(n)\)

\(ACcode: by \space WAduck\)

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
vector<int>to[100001],d[100001];
queue<int>q;
int n,m,deg[100001];
bool vis[100001],ans[100001];
int findroot()
{
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        if(!deg[x])return x;
        for(int i=0;i<to[x].size();i++)
        {
            if(--deg[to[x][i]]==1)
            {
                q.push(to[x][i]);
            }
        }
        for(int i=0;i<d[x].size();i++)
        {
            if(--deg[d[x][i]]==1)
            {
                q.push(d[x][i]);
            }
        }
    }
    return 0;
}
void dfs(int x,int f)
{
    ans[x]=1;
    for(int i=0;i<to[x].size();i++)
    {
        if(!vis[to[x][i]]&&f!=to[x][i])
        {
            dfs(to[x][i],x);
        }
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<n;i++)
    {
        int x,y;
        cin>>x>>y;
        to[x].push_back(y);
        to[y].push_back(x);
        deg[x]++;
        deg[y]++;
    }
    for(int i=1;i<=m;i++)
    {
        int x,y;
        cin>>x>>y;
        d[x].push_back(y);
        deg[y]++;
        vis[x]=1;
    }
    for(int i=1;i<=n;i++)
    {
        if(deg[i]==1)q.push(i);
    }
    int root=findroot();
    dfs(root,0);
    for(int i=1;i<=n;i++)
    {
        cout<<ans[i]<<endl;
    }
    return 0;
}

L:請求

初始圖上有\(1\)個點編號為\(1\)

接下來有\(N\)個操作

操作\(B(build)\),有一個引數\(x\),表示新增加一個節點(編號為現在圖中節點個數),連向(雙向邊)編號為\(x\)的節點,如果\(x==-1\)則不連邊

操作\(Q(query)\),有一個引數\(x\),詢問編號為\(x\)的節點到圖中相對於\(x\)是最遠點的距離(或者說,將\(x\)作為根,詢問深度最大的節點的深度)

說道連邊,我就想起某\(LCT\)(其實本來是想著玄學做法的\(2333\))

我們可以注意到,如果每個\(build\)操作都去修改全部節點的話,\(build\)操作\(O(n)\),\(query\)操作\(O(1)\)

如果\(build\)不去修改,那每次查詢要遍歷全圖,\(build\)操作\(O(1)\),\(query\)操作\(O(n)\)

我當時想到這個的時候,我還在想能不能寫樹分塊。。。但是顯然不好寫啊(而且不一定真的能寫)

所以我們還是要討論正解做法

偉大的\(Z\color{red}n\)學長曾經說過:"\(L\)是\(LCT\)維護直徑啊","\(sb\)題"

於是我們引入一條重要的結論,樹的直徑的端點之一一定是樹上任意一個點的最遠點之一

讀者自證不難(我覺得我說出來的太拗口了)

然後我發現我不會\(LCT\)

不過我們思索一下,我們究竟需要維護什麼?

維護點之間的距離,插入葉子節點

如果學過樹鏈剖分或者做過其他題,應該知道其實是維護兩個點的\(LCA\)

支援動態插入的\(LCA\)演算法,其實倍增就行了

倍增\(LCA\)的碼風是受了洛谷\(LCA\)第一篇題解感染的

int LCA(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    while(dep[x]>dep[y])
    {
        x=fa[x][lg[dep[x]-dep[y]]-1];
    }
    if(x==y)return x;
    for(int i=lg[dep[x]]-1;i>=0;i--)
    {
        if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
    }
    return fa[x][0];
}
int range(int x,int y)
{
    return dep[x]+dep[y]-2*dep[LCA(x,y)];
}

然後對於每個\(query\),顯然答案為\(max(range(x,l/*樹的直徑左端點*/),range(x,r/*樹的直徑右端點*/))\)

問題就是如何動態修改樹的直徑

對於每個\(build\)操作,樹的直徑只有三種情況

一,不變,\(l=l,r=r\)

二,左端點變為\(x,l=x,r=r\)

三,右端點變為\(x,l=l,r=x\)

其實我們只需要比較剛剛這三種情況的樹的直徑的大小就好了

然後由於有不連邊操作,我們多開一個\(g\)陣列記錄這個點的在哪顆樹上

void link(int x)
{
    ++idx;//點的編號
    if(x==-1)
    {
        g[idx]=idx;
        fa[idx][0]=idx;
        l[idx]=r[idx]=idx;
        return;
    }
    fa[idx][0]=x;
    dep[idx]=dep[x]+1;
    g[idx]=g[x];
    for(int i=1;(1<<i)<=dep[idx];i++)
    {
        fa[idx][i]=fa[fa[idx][i-1]][i-1];
    }
    int dis1=range(l[g[idx]],r[g[idx]]),dis2=range(idx,l[g[idx]]),dis3=range(idx,r[g[idx]]);
    if(dis2>dis1&&dis2>=dis3)r[g[idx]]=idx;
    if(dis3>dis1&&dis3>dis2)l[g[idx]]=idx;
}

請注意最後的判斷語句不要寫成\(dis2>=dis和dis3>=dis2\)或者是\(dis2>dis和dis3>dis2\)(想一想為什麼),我覺得其實就我這個傻子這裡還寫掛了一次

\(ACcode: by \space WAduck\)

#include<iostream>
#include<cmath>
using namespace std;
int n,idx;
int l[100001],r[100001],fa[100001][37],dep[100001],g[100001],lg[100001];
int LCA(int x,int y)
{
    if(dep[x]<dep[y])swap(x,y);
    while(dep[x]>dep[y])
    {
        x=fa[x][lg[dep[x]-dep[y]]-1];
    }
    if(x==y)return x;
    for(int i=lg[dep[x]]-1;i>=0;i--)
    {
        if(fa[x][i]!=fa[y][i])x=fa[x][i],y=fa[y][i];
    }
    return fa[x][0];
}
int range(int x,int y)
{
    return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
void link(int x)
{
    ++idx;
    if(x==-1)
    {
        g[idx]=idx;
        fa[idx][0]=idx;
        l[idx]=r[idx]=idx;
        return;
    }
    fa[idx][0]=x;
    dep[idx]=dep[x]+1;
    g[idx]=g[x];
    for(int i=1;(1<<i)<=dep[idx];i++)
    {
        fa[idx][i]=fa[fa[idx][i-1]][i-1];
    }
    int dis1=range(l[g[idx]],r[g[idx]]),dis2=range(idx,l[g[idx]]),dis3=range(idx,r[g[idx]]);
    if(dis2>dis1&&dis2>=dis3)r[g[idx]]=idx;
    if(dis3>dis1&&dis3>dis2)l[g[idx]]=idx;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        lg[i]=lg[i-1]+((1<<lg[i-1])==i);
    }
    for(int i=1;i<=n;i++)
    {
        char opt;
        int x;
        cin>>opt>>x;
        if(opt=='B')
        {
            link(x);
        }
        else
        {
            cout<<max(range(l[g[x]],x),range(x,r[g[x]]))<<endl;
        }
    }
    return 0;
}