Nordic Collegiate Programming Contest 2019 部分題解

Nordic Collegiate Programming Contest 2019 部分題解

前言,做國外的比賽感覺好像難度上比國內同時期的比賽要簡單一點,但是在這種情況下出線的國內的隊伍卻不能輕鬆wf捧杯,感覺有點迷惑,可能和國內競賽環境內卷嚴重有關。以後訓練儘量選擇一些正常的題目,避免無意義的trick和怪題,否則只能增強應試技巧,而對自身水平提升無益。

Flow Finder

題意

給定一棵有根樹,樹上每個點有一個點權。其中一些點的初始值已知,一些位置,現在把所有的未知點點權賦值(要求每個點都是正整數),並且滿足:每個點的權值等於其每個孩子的權值只和。特別地,如果方案不唯一或者沒有可行方案,輸出impossible,否則輸出這棵樹。

分析

這題可以本質上可以被歸納爲構造題。一個通用的解題思路是:先判斷在什麼情況下有解,然後在排除無解情況後進行構造,這樣可以大大降低思維和編碼的難度。

先分析何時輸出impossible。首先是有多個解的情況,不難發現,如果存在一條鏈從樹的葉子節點到根節點,他們的初始值都未知,那麼可以有無限種解決,因爲可以對他們同加同減。(這一步我看完題就發現了,這個主要基於對樹結構的認識,如果不能憑感覺發現的話,可以畫幾棵樹感受一下。)這說明如果存在“一根直腸通大腦,學多少忘多少”,那麼一定有無窮多組解,這是一個充分條件。

“直腸”
那麼在沒有“直腸”的情況下,是否還有可能有無窮多組解呢?如果沒有“直腸”,那麼一定存在一個界面,把根節點和所有葉子節點分來,並且這個界面上所有點都已知。

“界面”
顯然,在有"界面"的情況下, 界面以上的點權都變成確定的(也有可能出現衝突,事實上這構成了無解的情況,但是無論如何界面以上的任何結果不會產生多解)。只需要考慮界面下方。那麼能否從這個界面去把他的孩子也都確定下來呢?不難發現,如果存在一個點爲根,存在至少兩個孩子可以一路未知地通向葉子節點,那麼就有可能出現無窮多組解。

多個未知孩子
如上圖,紫色是一個已知的界面,紅色表示初始未知的節點,在這種情況下,因爲兩個孩子可以隨意分配,所以可能出現無窮解的情況。但是有一種例外情況,當這些紅色點的公共紫色父親的值恰好等於這些紅色點覆蓋到的葉子節點數量和其他與這些紅色節點相鄰的已知值之和時,結果是唯一的。 如上圖所示,如果這些紅色節點的父親是2+x(x爲最下層未被標紅的點的值)。這樣兩個紅色的葉子節點的值必定是1。

“界面”

如果上面那個描述有些拗口,那麼我們現在研究滿足一個點的權值等於其孩子之權值和的樹有何性質。因爲這個性質具有傳遞性,如果把一個節點分解爲其若干個孩子,那麼可以將其孩子再進行拆分。通過不停重複該操作,發現無論如何操作,都能得到一個“界面”。

我們現在給出界面的一個更加嚴格的定義:

我們稱在有根樹 TT 上的點集 SS 是關於點 vv 的一個界面,當且僅當:

  • sS\forall s\in S, ss 在以 vv 爲根的子樹中
  • 對任意在以 vv 爲根的子樹中的葉子節點 xxsS\exists s\in Sxx 在以 ss 爲根的子樹中
  • x,yS\forall x,y\in S, xx 不在以 yy 爲根的子樹中

事實上,在這裏,我們認爲界面是之前“界面”定義下所有可行點集的一個極小值,最小性由定義中的第三條保證(因爲如果一個點的在界面中,那麼以它爲根的子樹都已經被擋住了,他的孩子一定是一個冗餘項)。

我們可以發現界面的一個性質:

如果 SS 是關於點 vv 的一個界面,且在這棵樹上滿足一個點的權值等於其孩子之權值和,那麼 vv 的權值等於 SS 中所有點的權值之和。

根據上述拆分的操作,用數學歸納法容易證明。

界面們
其中每個顏色代表一個界面(紅色也可以被認爲是一個界面),並且各界面點權值和相等。

重新分析

在得到界面定義後,我們不難發現,對於任意一個未知的點,如果存在一個關於他的界面上所有點都已知,那麼這個點就可以被推斷出來。(除非發生衝突,那麼就無解)。重複這個操作若干次,使得所有可以通過以上方法求解的點都求出來。

剩下的未知點找不到一個界面,滿足其上所有點都已知,那麼就無法通過上述方法求解。如果一個點找不到界面,那麼一定存在一條連向葉子節點的每個點都未知的邊(不然就存在關於它的界面了)。

我們希望唯一確定這些未知點的值。對於一個點,如果我們找不到關於它的界面,那麼我們希望去找到一個點,使得它在這個界面中,並且滿足

  • 這是界面中唯一一個未知點
  • 或者,界面中所有未知點都是葉子界面,並且他們的值可以被唯一確定爲1

反過來考慮,在這一步時(即能找到對應已知界面確定的點都已經被確定),我們將任何一個有未知後繼的點拆分爲其所有孩子,然後遞歸地拆分他的所有未知孩子。這樣會產生一個關於他的一個界面,由已知點和未知葉子節點組成。如果只有一個未知葉子節點,那麼這個未知葉子節點可以被唯一確定;如果有多個未知節點,那麼這些未知節點個數必定等於這顆子樹的根節點權值減去所有界面中已知點的權值,並且這些未知葉子節點也可以被唯一確定出來。完成所有葉子節點的構造後,其他所有未知節點都被構造出來(因爲這樣他的未知祖先都能找到對應的界面了)。上述構造過程即證明了解的唯一性。在構造完成後沒有發生衝突,即證明了解決的可行性。所以答案正確。

對於不能由上述兩種方法構造出來的點,不難發現他們會產生多組解。

結論

我們重新歸納輸出impossible的情況:

  • 無解
    • 存在一個點,存在之上兩個界面上點已知,並且他們的權值和不同
  • 無窮組解
    • 對根節點不存在界面(直腸)
    • 存在一個點,其值已知,並且存在關於這個點的一個界面(除了這個點自身構成的界面),界面以下的點都已知,並且該點的點權不等於界面上未知點的數量加上已知點的點權和。

對於其他所有情況,都可以構造出來。

實現

按照前文的構造,一種顯而易見是進行三次dfs。第一次進行dfs,如果某個節點所有後繼已知,那麼這個節點也可以被唯一確定,用類似樹上dp的手段求解即可,在此過程種順便判斷是否產生衝突。第二次進行dfs,求出所有葉子節點。第三次dfs直接調用第一次dfs的函數即可。

但是不難發現,利用前文構造的手段,後面兩次dfs可以一次性實現,並且程序上更加簡潔。

對於每個節點,保存三個量:

  • pip_i 表示該點確定的值,如果爲止則爲0
  • sis_i 表示該點所有已知的孩子的權值和
  • ssiss_i 表示該點在不斷拆分未知孩子直到葉子的情況下產生界面上已知節點的權值和

我們在第一次dfs的時候預處理這三項,順便處理衝突。

void dfs1(int u){
    for(auto v:g[u]){
        dfs1(v);
        if(p[v]){//如果孩子已知(包括被構造出來的已知)
            s[u]+=p[v];//已知孩子
            ss[u]+=p[v];//已知孩子一定在拆分界面中,因爲根據定義他們不會被拆分
        }else{
            pre[u].push_back(v);//記錄未知孩子
            ss[u]+=ss[v];//“拆分界面已知權值和”可以分治
        }
    }
    if(g[u].empty()){//如果是葉子節點
        ss[u]=1;//那麼對他的祖先的貢獻只可能是1
    }else if(pre[u].empty()){//如果不是葉子節點,且沒有未知後繼
        if(p[u]&&p[u]!=s[u]){//那麼他的權值唯一,判斷是否衝突
            cout<<"impossible\n";
            exit(0);
        }
        if(!p[u])p[u]=s[u];//不衝突的話他的權值也確定了,就是他的所有孩子之和(因爲都已知)
    }
}

然後第二次dfs構造其他未知點,每個已知的父親可以直接求出其所有未知孩子。

void dfs2(int u){
    if(!p[u]){//如果走到某個點的時候還未知,那麼說明有無窮組解
        cout<<"impossible\n";
        exit(0);
    }
    if(pre[u].size()==1){//如果只有一個未知孩子
        int v=pre[u][0];
        p[v]=p[u]-s[u];//那麼就應該是父親的已知值減去他的所有孩子的值
        if(p[v]<=0){//如果不是正整數則衝突
            cout<<"impossible\n";
            exit(0);
        }
    }
    if(pre[u].size()>1){//如果有多個未知孩子
        if(ss[u]!=p[u]){//那麼因爲往下衍生得到的未知葉子只能是1,所以直接判斷這個虛構的界面是否合法
            cout<<"impossible\n";
            exit(0);
        }
        for(auto v:pre[u]){//如果合法,那麼每個未知孩子的值就是其對應虛構的界面的值
            p[v]=ss[v];
        }
    }
    for(auto v:g[u]){//遞歸求解
        dfs2(v);
    }
}

最後輸出所有 pip_i 即可。

Game of Gnomes

題意

nn 個人,分成 mm 組,每個人每回合可以造成人數點傷害。對手每回合可以從某組中殺死 kk 個人,如果組內人數不滿 kk , 那麼就把本組殺光。問如何佈陣能夠造成的總傷害最高。

分析

根據鴿籠原理,如果人數大於 m(k1)m*(k-1) 的話,那麼至少有一個人數大於 kk 。智力正常的對手一定會先殺那 kk 個人。對於 n>m(k1)n>m*(k-1) 的情況,可以直接暴力不行砍 kk 直到範圍足夠小。

那麼接下來考慮 nm(k1)n\leq m*(k-1) 的情況。方便起見,不妨先假設其中每組人數都不到 kk

不難發現,對方總是從大往小砍,總共砍 mm 刀。如果你人分佈的不均勻的話,因爲砍的總刀數一定,所以每次剩下的人都比均勻放置要少,所以在這種情況下一定是均勻放置最優。即,nn 人分 mm 組,那麼其中 n%mn\%m 組放 ceil(n/m)ceil(n/m) 人,mn%mm-n\%m 組放 floor(n/m)floor(n/m) 人。

這樣就是先砍若干刀 kk , 再砍若干刀 ceil(n/m)ceil(n/m),再砍若干刀 floor(n/m)floor(n/m)。造成傷害總和是一個變差的等差數列,可以 O(1)O(1) 求和。

現在我們去除每組人數不過 kk 的假設,那麼不難發現,如果某組人數大於 kk ,那麼對手一定會先砍 kk 個。這裏有點獻祭的意思,因爲在對手砍這 kk 個人的時候,其實是增加了攻擊輪次,這樣後面所有人多打一輪,可能會產生更高的傷害。

本題中因爲 m107m\leq10^7,直接暴力枚舉獻祭幾個 kk 即可,剩下的人成爲新的 nn 套用上述公式。

思考

其實這個模型有點像有着單峯性質。因爲如果直接不獻祭任何一組 kk 那麼攻擊輪次少,多打一輪可能多造成很多傷害;如果獻祭很多組kk , 那麼就算輪數變多,後面傷害卻會變少,得不償失。所以總傷害關於獻祭組數很可能是一個單峯函數,可以用三分法求解。但是因爲其數據是離散的,可能不一定滿足嚴格的單峯性質,可以考慮三分+峯頂附件小規模遍歷。這樣複雜度可以從 O(m)O(m)降到 O(logm)O(log m)

https://nanti.jisuanke.com/t/45303
https://nanti.jisuanke.com/t/45305

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