我的A-C題解和心路歷程 # Accepted極限程式碼巔峰賽

congmingyige發表於2024-11-06

https://ac.nowcoder.com/acm/contest/91849

多年退役找工作選手膜拜眾出題大佬!!!出的題特別好!!!

A

尋找分開的若干個11..1串,比如串001110001111,就是1111111

首先,認為所有的0,都是識別正確的,這些0的下一個位置,都是人類。即0 next = Human

其次,對於11..1串,從右到左,它們可以這樣設定(H代表人類,B代表機器):

0 1 1 1 1 1
H B H B H B

11..1串最後一個字元,也就是從右到左的第一個字元,必定是B,因為它識別錯了,下一個位置應該是人類,因為有前提:0 next = Human,而只有機器人才會識別錯誤。然後逐漸交替,即B H B H B … …。

實際做題的時候,大概感受一下輸入樣例是怎麼得到輸出的,然後直接下手寫了。

#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define ULL unsigned long long

const LL mod_1=1e9+7;
const LL mod_2=998244353;

const double eps_1=1e-5;
const double eps_2=1e-10;

const int maxn=2e5+10;

string str;

int main()
{
    int T, n, i, result, cnt;
    cin>>T;
    while (T--)
    {
        cin>>n>>str;
        result = 0;
        n--;
        for (i=0; i<n; i++)
        {
            cnt = 0;
            while (i<n && str[i]=='1')
            {
                i++;
                cnt++;
            }
            result += (cnt+1)/2;
        }
        cout << result << endl;
    }
    return 0;
}

B

看榜單,這麼多人做對,而且時間比較短,猜測是比較短程式碼的題目。

我是稍微想了B沒思路,然後先做了C(159分時AC),再折回來做B。那時候時間就很緊張,剩下20分鐘。

然後靜下心,把這個程式碼寫了。還遇到1e6寫成1e5的問題,還有多組測試樣例陣列的問題。反正我就猜測有什麼問題,畢竟是最後1分鐘,把有可能發生錯誤的都試著改一下然後提交,甚至i-K也試著改成i-K-1i-K+1提交一下。運氣好,AC了。同時,感覺最近很多場線上比賽壓軸提交程式碼AC,很爽!

因為是\(10^{6}\)複雜度,所以可以處理的演算法不多,我想過的有DP,ST演算法,感覺時間、空間都挺超的,還有尺取法,等等。我一開始想的,就是貪心,然後逐漸演化而成。

相鄰的兩個k倍數段(第k * j個段和第k * (j + 1)個段),至少要間隔k個數,然後貪心不了。後來想了一下,哦哦,其實對於某個數(位置為P)作為第k * (j + 1)個段的開頭,它就可以以位置1 ~ P-K的任意一個數作為第k * j個段的結尾。記錄位置為1 ~ P作為第k * j個段的結尾時的字首權值之和,然後取它們的最大值:\(\max_{i=1}^{P}(PreSum_{i})\)。每次P位置+1時可以O(1)處理。注意哈,我說的字首權值PreSum不是字首和,而是k倍數段中的數的數值之和。

對於第k * j個段,若當前P位置處於這個段,那麼P+1位置可以選擇新增\(a_{P + 1}\)這個數字,同時這個數仍然處於第k * j個段,仍然可以繼續擴充

關鍵程式碼:

            start[i] = a[i];
            value = max(value, start[i-K]);
            start[i] = max(start[i], start[i-1] + a[i]);
            start[i] = max(start[i], value + a[i]);

程式碼細節:

  • \(10^{6}\)
  • 陣列每次的初始化(不然WA)
  • K=1無法分割段時的特殊處理(賽後再次測試得知,它是要單獨處理的,否則WA)
#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define ULL unsigned long long
 
const LL mod_1=1e9+7;
const LL mod_2=998244353;
 
const double eps_1=1e-5;
const double eps_2=1e-10;
 
const int maxn=1e6+10;
 
LL a[maxn], start[maxn];
 
int main()
{
    LL T,n,K, sum, i, value;
    memset(start, 0, sizeof(start));
    scanf("%lld", &T);
    while (T--)
    {
        scanf("%lld%lld", &n, &K);
        for (i=1;i<=n;i++)
            scanf("%lld", &a[i]);
        for (i=1;i<=n;i++)
            start[i] = 0;
        value = 0;
        for (i=1;i<=n;i++)
        {
            if (i<K)
                continue;
            start[i] = a[i];
 
            value = max(value, start[i-K]);
 
            start[i] = max(start[i], start[i-1] + a[i]);
            start[i] = max(start[i], value + a[i]);
 
            //start[1]~start[i-K]
        }
 
        if (K==1)
        {
            sum = 0;
            for (i=1;i<=n;i++)
                sum += a[i];
            printf("%lld\n", sum);
            continue;
        }
 
        sum = 0;
        for (i=K;i<=n;i++)
            sum = max(sum, start[i]);
        printf("%lld\n", sum);
    }
    return 0;
}

C

看到這道題,我感受到了可能和這道題有點像,2018 “百度之星”程式設計大賽 - 初賽(A)度度熊學佇列我那時的解法,這道題用list資料結構,把編號為 v 的佇列接在編號為 u 的佇列的最後面,可以做到O(1)。實際上這道題的資料結構不適用這道題哈。

\(p_{k}\)的數值,只考慮\(k\)這個位置的變化,那麼就要從後往前看。比如1->3->2->4,1這個位置,從後往前看(反過來看),swap(1,3)swap(3,2)swap(2,4),最後是一開始的a[4]的數值在1這個位置。感覺遇到過這種這類的題不少次。

然後問題來了,swap(X,Y),就是當前處於X的數處於Y的數交換位置。一個集合和一個集合交換位置,我想到的是像setlistvector這類的資料結構,都可以做到O(1)處理。

然後,就是記錄和獲取work(L,R,K)的數值。

實際上可能有很多個操作都在同一個位置,但是統一處理降低了複雜度。否則,比如位置1的數值經歷了m次修改,n個詢問都是問這個位置1,複雜度就是\(O(n*m)\)了。

同時,有些資料結構,無法獲取R->L,當到達位置L時,要獲取結果然後結束(處理剛好完成後,從某個資料結構中刪除)。所以,選擇的資料結構需要有排序 / Top的功能。

實際操作中:

  • set,記錄(L, Qth)Qth是第Qth個查詢。L就是題目中的L(L,R) rangeL
  • vector,記錄反過來(從大到小),作為開始的R,有哪些(L,R)對是從某個特定的R開始的。R就是題目中的R(L,R) rangeRR從大從小遍歷。
  • R從大從小遍歷,處理這個特定的R的若干序列(K,L,Qth)(L,Qth)加入set[K]中。
  • 你並不知道對於特定的(L,R,K),當L處理完後,它在哪個set中。因為setn個(\(n \leq 10^6\)),你總不能依次遍歷nset吧尋找吧。一個巧妙的處理點,對於已經在處理第I個操作,在swap(set[ a[i] ], set[ b[i] ])之前,對於第a[i]b[i]set,可以把之前已經處理完的對 刪除(erase)了。具體就看哪些對的L值大於現在的I。因為set是自動排序的資料結構,所以這些要處理的(L,Qth)實際上就在set的最後面幾個。

程式碼實際上不長哈,就是有點耗腦

因為很久沒寫set這類reverse的程式碼了,寫起來很難受,這個地方卡了很長時間。刪除一個元素的時候,指標也會發生變化。比如rbegin,erase(it),++it,等等,反正就是很容易寫錯。這是用文心一言(ChatGPT更好哈)跑"set刪除一個元素,它的next指標發生變化"生成的結果。你需要先記錄next指標,再刪除哈,或者每次刪除最大的元素並迴圈。我寫成it = --st[j].end(),讓我有點難崩,應該有其它的寫法吧,也可以用auto

在 C++ 的 std::set 容器中,元素的儲存和訪問方式與連結串列(如單連結串列或雙連結串列)有很大的不同。std::set 是一種基於紅黑樹(一種自平衡二叉查詢樹)實現的關聯容器。因此,std::set 中的元素不是透過指標連結在一起的,而是透過節點在紅黑樹中的位置關係來組織。

當你從 std::set 中刪除一個元素時,以下幾點需要注意:

元素指標的變化:在 std::set 中,你通常不會直接操作元素的指標(如 next 指標),因為 std::set 的內部實現是基於節點的,而這些節點之間的連結(父節點、左子節點、右子節點)是由紅黑樹的性質來維護的。

紅黑樹的調整:當你刪除一個元素時,紅黑樹需要進行一系列旋轉和重新著色操作,以保持其平衡性和查詢效率。這些操作會改變樹的結構,但你不會直接看到或操作這些“指標”(即節點之間的連結)。

除錯的時候,不知道為什麼,我的VSCode一直爆炸,每次除錯後,都得關閉重新開啟。我懷疑是set的問題。就像,之前CodeBlocks跑什麼發生錯誤後(比如段錯誤),這個軟體就得出點奇怪的問題,需要重新關閉開啟才能調教好。太難受啦!我是第一個遇到這個問題哈,我用VSCode也是因為它的程式碼自動補全(這場基本沒有看到)和CPH外掛(也沒太多用到,但稍微有點用)。

#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define ULL unsigned long long

const LL mod_1=1e9+7;
const LL mod_2=998244353;

const double eps_1=1e-5;
const double eps_2=1e-10;

const int maxn=1e6+10;

typedef pair<int, pair<int, int> > par;
vector<par> vec[maxn];
set<pair<int, int> > st[maxn];
set<pair<int, int> >::iterator it;

int result[maxn], a[maxn], b[maxn];
int n, m, q, L, R, K, x, y, z, i;

void handle(int j)
{
    if (st[j].empty())
        return;
    
    while (!st[j].empty())
    {
        it = --st[j].end();
        if (it->first > i)
        {
            result[it->second] = j;
            st[j].erase(it);
        }
        else
            return;
    }
}

int main()
{
    cin>>n>>m>>q;
    for (i=1;i<=m;i++)
        scanf("%d%d", &a[i], &b[i]);
    for (i=1;i<=q;i++)
    {
        scanf("%d%d%d", &L, &R, &K);
        vec[R].push_back(make_pair(K,make_pair(L, i)));
    }
    for (i=m;i>=1;i--)  //L/R range
    {
        for (int j : {a[i], b[i]})
            handle(j);
        
        for (auto temp : vec[i])
        {
            x = temp.first;
            y = temp.second.first;
            z = temp.second.second;
            
            st[x].insert(make_pair(y, z));  //L i
        }
        swap(st[ a[i] ], st[ b[i] ]);
    }
    i = 0;
    for (int j = 1; j<=n; j++)  // value range
        handle(j);

    for (i=1;i<=q;i++)
        printf("%d\n", result[i]);
    return 0;
}

P.S. 我覺得好的題的定義

我感覺這種很考思維邏輯,資料結構的題(我指的是前3題,後面沒看),ChatGPT很難生成正確的答案。其實AtCoder、CodeForces估計一直有這方面的嘗試和討論,出點很有創新的題目,而不是模板題。有一次我看到AtCoder歪榜,你懂的字數字數

我的個人感悟巴拉巴拉

實力尚還在吧,多年後,一些資料結構有新的感悟,但是一些STL函式使用的確生疏了,寫得也相對比較慢,但是程式碼錯誤基本會比較少。雖然一開始有點事,後面環境也不是太安靜,但是他們都努力降低聲音了,set也很久沒寫了很生疏,但是最後20分鐘趕上了第三題的ac,最後兩分鐘趕上了第二題的ac。看了一下榜,三題AC倒3,這很正常,因為我大大高估了自己做第三題的時間(STL字數字數,太耗時了),第二題最後做的,罰時超大,而且想到的都交了,於是5WA。前100名,187分鐘的罰時,大概理想就是10(15) 30 (40) 90(120)吧,實際上挺難的,你還要考慮WA的次數。距離拿衣服還有很遠的地步,但是足夠了,很開心。話說,拿衣服就不能放寬到200名嗎??!!!讓兄弟們混件衣服唄!!!感覺參加這次比賽的同學都挺有水平,看它們牛客Ranking和顏色就知道了,普遍ACM銀金水平吧。我很多牛客比賽都是亂作的,還有人很菜,然後藍色,嗯?黑人問號?尊貴的藍色?嗯???

相關文章