2022年藍橋杯軟件類省賽 C/C++ B組 解析


前兩年題解都是學長寫的,今年到我了QAQ

今年題目相比於去年的大致上變簡單了,但該坑的地方還是坑

題目和賽時代碼都沒存,提早溜了,所以本文內代碼可能大概或許似乎應該不一定完全和早上敲的一樣

我們學校是線下借機房進行比賽的,出來后網上沖浪划水才知道原來今年甚至有線上的(?)

寫這篇題解的過程中我逐漸想不起賽時到底在想些什么又寫了些什么現在很慌感覺好多東西現在能理清楚但忘了早上有沒有想清楚了嗚嗚嗚

最后再提醒一句,數據量大的題目用 \(cin/cout\) 記得關同步流,下面的代碼為了簡潔就省略掉了

\(UPD\)\(H\) 題代碼更改。
\(UPD\ 2\): 加入了題面。
\(UPD\ 3\)\(E\) 題補充了對於負數的討論,但結論不變。
\(UPD\ 4\) - 2022/12/17: \(E\) 題代碼錯誤變更(官網數據太弱了吧),\(J\) 題代碼更改+坑點解釋。


A - 九進制轉十進制

prob_a

\((2022)_9 = 2*9^3+0*9^2+2*9^1+2*9^0 = 1478\)

簽到成功!


B - 順子日期

prob_b

這比賽每年都會有那么一兩道啥b題,習慣了,自以為提出了一個妙妙的東西,然后題目里甚至一點都不進行解釋,出題的就擱這擺

茲認為,這題是全場最啥b題

“順子指的就是連續的三個數字”:連續遞增?連續遞減?

“例如 \(20220123\) 就是一個順子日期,因為它出現了一個順子:\(123\)”:出現了一個順子 \(123\),難道表示 \(012\) 不是順子?

所以這題就純純的在猜謎語

至於答案呢,如果 \(012\) 不算,就只有可能是 \(123\),用手指掰一掰都能枚舉出來就 \(0123,1123,1230,1231\) 這四種月日組合情況,答案 \(4\)

如果 \(012\) 算,上面四種加上 \(012x\)\(1012\),答案 \(14\)

這題大家應該要么 \(4\) 要么 \(14\) 吧,反正我寫的 \(14\)最后高低還得罵兩句


C - 刷題統計

prob_c

原題?這種題目 CF 上感覺都做過好多次,C 語言入門題

以一周為單位,一周做 \(5a+2b\) 道題,故天數至少 \(\lfloor\frac n {5a+2b}\rfloor \times 7\)

然后讓 \(n\)\(5a+2b\) 取模,最后一周暴力跑跑就行

void solve()
{
    ll a,b,n;
    cin>>a>>b>>n;
    
    ll d=5*a+2*b;
    ll ans=n/d*7;
    n%=d;
    
    for(int i=1;i<=5;i++)
        if(n>0)
        {
            n-=a;
            ans++;
        }
    for(int i=1;i<=2;i++)
        if(n>0)
        {
            n-=b;
            ans++;
        }
    cout<<ans<<'\n';
}

D - 修建灌木

prob_d

明顯對於第 \(i\) 個位置的灌木,要么是愛麗絲修剪完它后向左走再走回來剪,要么就是剪完向右再回來

要求最高的高度,那就向左向右取個最大距離乘上 \(2\) 就是答案了

假設剪完 \(i\) 后向左,下一次再剪 \(i\) 就是 \((i-1)\times 2\) 天后;

假設剪完 \(i\) 后向右,下一次再剪 \(i\) 就是 \((n-i)\times 2\) 天后。

void solve()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        cout<<max(i-1,n-i)*2<<'\n';
}

E - X 進制減法

prob_e1
prob_e2
prob_e3

證明:

因為兩個數使用同一進制規則,假設第 \(i\) 位為 \(a_i\) 進制 (從低往高,假設 \(i=0\) 為最低位)

再定義 \(b_i = \prod_{j=0}^{i-1} a_j\)

明顯,對於該規則下的進制數 \(A\),其十進制值就是 \(A_0b_0+A_1b_1+\cdots+A_nb_n\)

同樣的,\(B\) 的十進制值就是 \(B_0b_0+B_1b_1+\cdots+B_mb_m\)

(這里注意,題目里保證了 \(A\gt B\),且使用同一進制規則,因此 \(n\ge m\) 恆滿足)

現在我們要讓 \(A-B\) 的結果盡可能小

兩式子相減,得到 \((A_0-B_0)b_0+(A_1-B_1)b_1+\cdots+(A_m-B_m)b_m+A_{m+1}b_{m+1}+\cdots+A_nb_n\)

已知 \((A_0-B_0),\ (A_1-B_1)\) 這些值是定值,如果是正數,那我們只能是讓 \(b_i\) 取得盡可能小,也就是讓上面假設的 \(a_i\) 盡可能小

但如果對於某個位置 \((A_i-B_i)\) 是負數呢?負數的話我們就應該讓 \(b_i\) 盡可能大啊?

其實根據題目 \(A\ge B\) 的限制可知,一定會有更高位 \(j\gt i\) 使得 \((A_j-B_j)\) 是正數

又因為 \(b_i\) 是由 \(a_0\) 連乘到 \(a_{i-1}\) 的,所以較低位 \(b_i\) 如果取得越大,那么 \(b_{i+1},b_{i+2}\) 等更高位也是會跟着變大的,且變大的級數較低位而言更大

明顯這樣子兩數相減的結果會因為高位變大而變大,所以不論 \((A_i-B_i)\) 正負如何,進制都應當盡可能小

綜上,讓每個位置的進制等於 \(A,B\) 兩個數該位置的較大值 \(+1\) 就是答案

貪心:

\(i\) 位置的進制 \(a_i = \max(A_i,B_i) + 1\),注意最低進制為 \(2\),再與 \(2\) 取個 \(\max\)

然后在取模意義下單獨把 \(A,B\) 兩個數的值求出來,\((A-B+mod)\% mod\) 就是答案

給定數據的第一個數 \(n\) 只是提了一下最大進制,但實際上沒用

還有就是坑點在長度不相同的時候,多造些例子測測,我忘了當時怎么寫的了現在很慌,但跟下面這份代碼應該不一樣

// 忘了數據范圍多大了,數組隨便開開
const ll mod=1e9+7;

int x,n,m;
int A[100050],B[100050];
int C[100050];

void solve()
{
    cin>>x;
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>A[i];
    cin>>m;
    for(int i=1;i<=m;i++)
        cin>>B[i];
    
    int len=max(n,m);
    for(int i=len;i>=1;i--)
    {
        C[i]=2;
        if(n-(len-i)>0)
            C[i]=max(C[i],A[n-(len-i)]+1);
        if(m-(len-i)>0)
            C[i]=max(C[i],B[m-(len-i)]+1);
    }
    
    ll AA=0,BB=0;
    for(int i=1;i<=len;i++)
    {
        if(n-(len-i)>0)
            AA=(AA*C[i]+A[n-(len-i)])%mod;
        if(m-(len-i)>0)
            BB=(BB*C[i]+B[m-(len-i)])%mod;
    }
    
    cout<<(AA-BB+mod)%mod<<'\n';
}

F - 統計子矩陣

prob_f1
prob_f2
prob_f3

又是一道既視感極強的題目呢

由於題目數據保證每個位置的值為非負數,不需要考慮那么多

二維前綴和預處理一下,然后 \(O(n^2)\) 枚舉子矩陣左上角 \((x,y)\)

如果考慮暴力,再 \(O(n^2)\) 枚舉右下角,對於 \(n=500\) 的數據明顯不大行

假設子矩陣當前只有一行,且滿足條件的子矩陣右端點能到達坐標 \((x,r)\)

那么這一行滿足條件的子矩陣數量就是 \(r-y+1\)

考慮往下擴一行,明顯 \((x,y)\)\((x+1,r)\) 的和變大了,因此通過前綴和來維護右邊界 \(r\),嘗試左移到第一個位置滿足 \((x,y)\sim(x+1,r)\) 的和滿足條件位置,答案還是 \(r-y+1\)

可以發現右邊界 \(r\) 隨着下邊界的下移是單調遞減的

繼續這樣做下去,直到枚舉到第 \(n\) 行,或者 \(r\lt y\) 為止

這種做法的時間復雜度為 \(O(n)\)

總時間復雜度為 \(O(n^3)\) ,勉強可以過,注意寫法即可

int a[505][505];
ll s[505][505];

void solve()
{
    int n,m,k;
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
    ll ans=0;
    for(int i=1;i<=n;i++) // 上
        for(int j=1;j<=m;j++) // 左
        {
            int r=m; // 右
            for(int u=i;u<=n;u++) // 下
            {
                while(s[u][r]-s[i-1][r]-s[u][j-1]+s[i-1][j-1]>k)
                    r--;
                if(r<j)
                    break;
                ans+=r-j+1;
            }
        }
    cout<<ans<<'\n';
}

G - 積木畫

prob_g1
prob_g2
prob_g3

考慮 DP

定義 \(dp[i=1\sim n][j=0\sim 2]\) 表示往前 \(2\times i\) 的畫布中填充積木的方案數

其中 \(j=0\) 時表示第 \(i\) 列上下兩個格子都被填充了,且 \(i-1\) 列之前的所有格子都全部已填充

\(j=1\) 表示第 \(i\) 列上邊的格子沒有被填充,且 \(i-1\) 列之前的所有格子都全部已填充

\(j=2\) 表示第 \(i\) 列下邊的格子沒有被填充,且 \(i-1\) 列之前的所有格子都全部已填充

考慮初始狀態:

\(dp[1][0]=1\)

\(dp[2][0]=2,\ dp[2][1]=dp[2][2]=1\)

考慮狀態轉移:

  • \(dp[i][0]\) 可以由下圖中四種狀態轉移而來

pic_g1

第一種的前置情況等同於 \(dp[i-1][0]\)

第二種的前置情況等同於 \(dp[i-2][0]\)

第三種的前置情況等同於 \(dp[i-1][1]\)

第四種的前置情況等同於 \(dp[i-1][2]\)

  • \(dp[i][1]\) 可以由下圖中兩種狀態轉移而來

pic_g2

左邊的前置情況等同於 \(dp[i-2][0]\)

右邊的前置情況等同於 \(dp[i-1][2]\)

  • \(dp[i][2]\) 與上圖類似,上下翻轉一下

兩種情況分別是 \(dp[i-2][0]\)\(dp[i-1][1]\)

然后就可以直接敲代碼了

const ll mod=1e9+7;

ll dp[10000050][3];

void solve()
{
    int n;
    cin>>n;
    dp[1][0]=1;
    dp[2][0]=2;
    dp[2][1]=dp[2][2]=1;
    for(int i=3;i<=n;i++)
    {
        dp[i][0]=dp[i-1][0]+dp[i-2][0]+dp[i-1][1]+dp[i-1][2];
        dp[i][1]=dp[i-2][0]+dp[i-1][2];
        dp[i][2]=dp[i-2][0]+dp[i-1][1];
        dp[i][0]%=mod;
        dp[i][1]%=mod;
        dp[i][2]%=mod;
    }
    cout<<dp[n][0]<<'\n';
}

悄咪咪地考慮一下小優化

打個小表:\(1,2,5,11,24,53,117,258,569,1255\)

猜想一下,這個數列應該滿足 \(a_i=2a_{i-1}+x\) 這個樣子的

然后再手算一下,發現式子就出來了

\(a_i=2a_{i-1}+a_{i-3}\)

於是常數就變小了~

const ll mod=1e9+7;

ll a[10000050];

void solve()
{
    int n;
    cin>>n;
    a[1]=1;
    a[2]=2;
    a[3]=5;
    for(int i=4;i<=n;i++)
        a[i]=((a[i-1]<<1)+a[i-3])%mod;
    cout<<a[n]<<'\n';
}

H - 掃雷

prob_h1
prob_h2
prob_h3

首先,數量的數據范圍比較大,不能像傳統方式那樣 \(O(n^2)\) 來求某個圓覆蓋哪些點

但是發現 \(r\) 很小,於是考慮暴力求 \((x\pm r,y\pm r)\) 這一范圍內有哪些點就行啦

然后題目又說同一點上可能存在多個點

於是使用 map<Point,Rmax> 存儲圓心位於點上時的最大半徑,另一個 map 存有多少個圓圓心位於該點

遍歷每個炸彈可能炸到的點,如果該點存在地雷,則從該點繼續 dfs 搜索即可

typedef pair<int,int> P;

map<P,int> mpr,mpcnt;
int ans=0;

inline bool isPointOnCircle(int xx,int yy,int x,int y,int r)
{ // 盡可能不要用浮點數計算
    return (xx-x)*(xx-x)+(yy-y)*(yy-y)<=r*r;
}

void dfs(int x,int y,int r)
{
    int xmn=x-r,xmx=x+r;
    int ymn=y-r,ymx=y+r;
    for(int xx=xmn;xx<=xmx;xx++)
        for(int yy=ymn;yy<=ymx;yy++) // 枚舉2r*2r大小的矩陣內所有點
        {
            if(!isPointOnCircle(xx,yy,x,y,r))
                continue;
            P point=P(xx,yy);
            if(mpr.count(point)) // 如果該點地雷未被訪問過
            {
                ans+=mpcnt[point];
                int rr=mpr[point];
                mpr.erase(point); // 刪掉這個鍵值,表示訪問過了
                dfs(xx,yy,rr);
            }
        }
}

void solve()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int x,y,r;
        cin>>x>>y>>r;
        mpr[P(x,y)]=max(mpr[P(x,y)],r);
        mpcnt[P(x,y)]++;
    }
    for(int i=1;i<=m;i++)
    {
        int x,y,r;
        cin>>x>>y>>r;
        dfs(x,y,r);
    }
    cout<<ans<<'\n';
}

I - 李白打酒加強版

prob_i1
prob_i2
prob_i3

標准滾動數組 DP(也可以不滾動直接DP),或者直接記搜也可以

定義數組 \(dp[s][t][a][b]\) 表示前 \(s\) 次事件中,遇到了 \(a\) 次店、\(b\) 次花,酒還剩 \(t\) 斗的方案數

初始狀態 \(dp[0][2][0][0]=1\)

事件最多 \(n+m=200\) 次,酒的次數應當不超過還剩余的遇見花的次數,最大為 \(100\)

但這樣數組大小就是 \((n+m)\times n\times m^2\),空間太大了

發現對於合法的 \(dp[s][t][a][b]\)\(s=a+b\) 是恆成立的,且每次狀態轉移都是 \(s+1\),然后 \(a\) 或者 \(b\) 加上 \(1\)

因此可以讓 \(s\) 進行滾動,且后面的狀態不會引用兩步及之前的狀態,轉移前也不需要清空滾動數組

轉移的話就很簡單了,考慮轉移到 \(dp[s][t][a][b]\) 的前置狀態:

  • \(dp[s-1][\frac t 2][a-1][b]\) 可以成為前置狀態,條件是 \(t\) 為偶數且 \(a\gt 0\)
  • \(dp[s-1][t+1][a][b-1]\) 可以成為前置狀態,條件是 \(b\gt 0\)

最后,注意一下題目里提及最后一次遇見的是花,因此答案應該是 \(dp[n+m-1][1][n][m-1]\) 而不是 \(dp[n+m][0][n][m]\)當時我還以為自己敲錯了

UPD: s,a,b 三維直接去掉一維也可以QAQ,那就不用滾動了

const int mod=1e9+7;

int dp[2][101][101][101];

void solve()
{
    int n,m;
    cin>>n>>m;
    dp[0][2][0][0]=1;
    for(int s=1;s<n+m;s++)
        for(int t=0;t<=min(m,n+m-s);t++) // 酒的斗數不能超過接下來的步數,下面還能再縮小范圍
        {
            int cur=s&1,pre=cur^1;
            for(int a=0;a<=n;a++)
            {
                int b=s-a;
                if(b>m||b<0||t>m-b) // 如果b不合法或者剩余的酒太多了
                    continue;
                if(!(t&1)&&a>0)
                    dp[cur][t][a][b]+=dp[pre][t/2][a-1][b];
                if(b>0)
                    dp[cur][t][a][b]+=dp[pre][t+1][a][b-1];
                dp[cur][t][a][b]%=mod;
            }
        }
    cout<<dp[(n+m-1)&1][1][n][m-1]<<'\n';
}

J - 砍竹子

prob_j1
prob_j2
prob_j3

最后一題了,本來還以為只能整個區間 DP 啥的騙騙分呢,結果怎么也想不出來怎么 DP

然后換了一種思路想了一遍,發現直接把相鄰的嘗試合並就可以了(

合並的意思是這樣子的,比如對於兩個相鄰位置,一個值是 \(6\) ,另一個值是 \(7\)

根據題意,\(6\) 單獨進行操作,過程是 \(6\rightarrow 2\rightarrow 1\)

\(7\) 單獨進行操作,過程是 \(7\rightarrow 2\rightarrow 1\)

這兩個數字在操作過程中均出現了 \(2\),也就說明我們可以將這兩個不同的數先單獨操作,使其都變成 \(2\),然后兩個就可以合並起來看一起操作了

因此可以直接從左往右看相鄰的數,如果左邊的數在操作過程中出現的數字與右邊的數操作過程中出現的數存在相同,那么就可以先單獨進行操作,再一起進行操作

就比如樣例,先單獨將過程畫出來,如下圖左,然后在處理數字 \(6\) 時,發現其處理過程存在 \(2\),與其左側的數有重疊部分,那么我們就只把 \(6\rightarrow 2\) 這一過程中使用的次數加入答案即可;數字 \(7\) 同理,因此下圖中被圈起來的箭頭就是計入答案的數量

pic_j1

最后,題目給定的式子操作次數是不超過 \(\log\) 級別的,暴力跑一下 \(10^{18}\) 可以發現只需要執行 \(6\) 次,所以查找重疊數字的這一步就直接 map/set 暴力整即可

總體復雜度 \(O(n\times 6\log 6)\)

UPD:注意,有大部分測試點卡了 sqrt 函數,所以這部分需要手寫一個整數二分。

typedef long long ll;

ll a[200050];
map<ll,int> mp;

int SQRT(ll d)
{
    int l=0,r=1e9;
    while(l<=r)
    {
        int m=(l+r)/2;
        if(1LL*m*m<=d)
            l=m+1;
        else
            r=m-1;
    }
    return r;
}

int trans(ll d)
{
    return SQRT(d/2+1);
}

int sol(ll d) // 查詢需要多進行的操作數
{
    int r=0;
    while(d>1)
    {
        if(mp.count(d))
            return r;
        d=trans(d);
        r++;
    }
    return r;
}

void ins(ll d) // 將map改為d的變化過程
{
    mp.clear();
    while(d>1)
    {
        mp[d]=1;
        d=trans(d);
    }
}

void solve()
{
    int n,ans=0;
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        ans+=sol(a[i]);
        ins(a[i]);
    }
    cout<<ans<<'\n';
}

就無了,祝好運,哭哭


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM