1. 程式人生 > 其它 >【題解】8 月 24 日模擬賽題解

【題解】8 月 24 日模擬賽題解

前言

\(\texttt{Nothing lasts forever}\)
\(\texttt{No tears of rain fall forever}\)

暑假的倒數第二場模擬賽,一如既往的爆炸。或許 爆炸 一詞已經無法勾畫出我慘不忍睹的成績了,可能我的實力只能拿到這個分數。爆炸——或許我本就無法拿到那麼高的分數吧。

現在看來,每一場模擬賽給我的感覺似乎都差不多。早上買咖啡,在機房吃早餐,順便看看部落格打打板子,然後對著本校 \(IOI\) 神仙學長的簽名頂禮膜拜。考試開始點開題目一看大部分都是簡單題,花不到一個小時全部寫完以後開難題。這樣吧,遇到難題基本就變成智障。離譜的是這個時候我一定會選擇回去對拍,對,直接棄掉。最後在考試結束前發現有一道簡單題寫炸,然後毒瘤題就一定會突然產生思路。放榜以後瞄一眼,另一道簽到題又炸了,毒瘤題來不及寫程式碼所以只有暴力分。

其實每次放榜以後看到慘不忍睹的 \(rk\) 都總是會在心裡安慰自己:反正到時候考場時間夠,多檢查幾遍程式碼再隨便拍一拍就行了。毒瘤題又不是沒思路,考場上面抓緊一點還是能夠寫出來的……然後在這個迴圈中徘徊,最後直接在考場上面爆炸,來年再戰。

我十分清楚這種狀態會把我拉下深淵,無論是心態上的浮躁還是實力上的落後都間接或直接地導致了這幾次模擬賽的失利。感覺有必要好好反省一下了,但是不是在這篇部落格(怎麼可能給你們看呢:)

算了,不立 \(flag\) 了。考完 \(CSP\) 回來更新一下吧,到時候把 \(CSP\) 的遊記也貼出來。希望可以拿到藍勾(

\(\texttt{T1}\) 最值序列

題目大意

給您一個長度為 \(n\) 的序列 \(a_i\) 和一個初始值為 \(0\) 的變數 \(A\)。現在您每次可以從 \(a_i\) 中選出一個未被操作過的數,並將 \(A\) 賦值為 \(A + a_i\) 或者 \(A \times a_i\)。試求 \(n\) 次操作後變數 \(A\) 的最大值。

\(n \leq 5 \times 10^5\)\(n\) 為偶數,\(1 \leq a_i \leq 10^9\)

解題思路

這道題似乎在 \(CF\) 上見過的樣子,好像是 \(800+\) 的題。看到題目中的 最大值 可以自然聯想到 貪心。題目中已經提示了 \(n\) 為偶數,那麼我們有一個顯然的貪心:先將 \(A\)

加上前 \(\frac{n}{2}\) 小的數,然後讓 \(A\) 乘上前 \(\frac{n}{2}\) 大的數。換言之設 \(b\)\(a\) 升序排序後的陣列,那麼最終 \(A = (\sum\limits_{i = 1}^{n \div 2} b_i) \times \prod\limits_{j = n \div 2 + 1}^{n} b_j\)。具體證明可以參考 @lsw1 的數學證明,這裡不再贅述。

參考程式碼

#include <cstdio>
#include <algorithm>
using namespace std;
 
const int maxn = 5e5 + 5;
const int mod = 998244353;
 
int n;
int a[maxn];
 
signed main()
{
    long long ans = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++)
        a[i] %= mod;
    for (int i = 1; i <= n / 2; i++)
        ans = (ans + a[i]) % mod;
    for (int i = n / 2 + 1; i <= n; i++)
        ans = (ans * a[i]) % mod;
    printf("%lld\n", ans);
    return 0;
}

\(\texttt{T2}\) 莫的難題

題目大意

現在有 \(1, 2, 3, 5, 9\)\(5\) 個數字和 \(t\) 個詢問。已知給出的五個數字可以任意組合成新數,且相同的數字可以重複使用。對於每一次詢問給出的 \(n, m\),試求出可以組合成的數中第 \(C_n^m\ mod\ 10^9 + 7\) 大的數。

\(1 \leq t \leq 1000, 1 \leq m \leq n \leq 100\)

解題思路

這道題應該可以看作是 模擬 題了。\(C_n^m\ mod\ 10^9 + 7\) 必須求出,觀察資料範圍 \(1 \leq m \leq n \leq 100\),發現 \(\mathcal{O}(n ^ 2)\) 演算法可以接受,因此考慮使用 楊輝三角 來計算組合數。當然當 \(n, m\) 較大時可以考慮採用 逆元 來計算組合數,時間複雜度相對更優,此處不作討論。

回到正題。已知數 \(x\) 的排名 \(rk\),現在需要求出數 \(x\) 的值。這裡處理的思路類似於 逆康託展開,我們發現一個數的排名等於比其小的數的個數加一。不妨設數 \(x\) 共有 \(N\) 位,\(\forall 1 < x \leq N, cnt_i\) 表示前 \(x - i + 1\) 位與數 \(x\) 相同且比數值比 \(x\) 小的數的個數。那麼 \(\sum\limits_{i = 2}^N cnt_{x - i + 1}\) 即為答案。

如果暴力求出 \(cnt\) 顯然是不可取的,因此我們考慮 \(\mathcal{O}(1)\) 計算單個 \(cnt\)。我們發現 \(\sum\limits_{i = 2}^N\) 可以被寫成類似於五進位制 位權求和 的形式。換言之,我們發現如果把排名 \(rk\) 轉換成五進位制以後,第 \(i\) 位上的數恰好是前 \(x - i\) 位與數 \(x\) 相同且數值小於 \(x\) 的數的個數減一,那麼我們直接把 \(rk\) 轉化成五進位制,並把每一位對映到 \(1, 2, 3, 5, 9\) 中對應的數字即可。

注意我們實際上用數字 \(0\)\(4\) 分別表示 \(1, 2, 3, 5, 9\)。假設當 \(rk = 5\) 時直接轉換成五進位制會變成 \(10\),但是實際上排名為 \(5\) 的數是 \(9\)。因此我們在轉換成五進位制時需要將 \(rk\) 不斷減一。詳見程式碼。

參考程式碼

#include <cstdio>
#include <cmath>
using namespace std;
 
const int maxn = 1e2 + 5;
const int mod = 1e9 + 7;
 
int t, n, m;
int res[20];
int tar[5] = {1, 2, 3, 5, 9};
long long c[maxn][maxn];
 
int read()
{
    int res = 0, flag = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9')
    {
        if (ch == '-')
            flag = -1;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9')
    {
        res = res * 10 + ch - '0';
        ch = getchar();
    }
    return res * flag;
}
 
int main()
{
    int rk, len;
    c[0][0] = 1;
    for (int i = 1; i <= 100; i++)
    {
        for (int j = 0; j <= i; j++)
        {
            c[i][j] = c[i - 1][j];
            if (j > 0)
                c[i][j] = (c[i][j] + c[i - 1][j - 1]) % mod;
        }
    }
    t = read();
    while (t--)
    {
        n = read(), m = read();
        len = 0, rk = c[n][m];
        while (rk > 0)
        {
            rk--, res[++len] = rk % 5;
            rk /= 5;
        }
        for (int i = len; i >= 1; i--)
            printf("%d", tar[res[i]]);
        puts("");
    }
    return 0;
}

\(\texttt{T3 Seek the Name, Seek the Fame}\)

題目大意

題目連結

給定若干字串 \(S_i\),對於每一個字串,試求所有既是 \(S\) 的字首又是 \(S\) 的字尾的字串的長度,遞增輸出。

\(1 \leq \sum\limits |S_i| \leq 4 \times 10^5\)

解題思路

看到字串和前後綴考慮使用 \(KMP\)\(KMP\) 中的 \(nxt\) 陣列存在一個重要的性質:對於字串 \(S\),長度為 \(nxt_{|S|}\) 的字首和與其長度相等的字尾完全相同。具體到這道題中,顯然長度為 \(nxt_{|S|}\) 的字首符合條件。下一個滿足條件且最長的字串應該是字串 \(S\) 長度為 \(nxt_{|S|}\) 的字首的字首,同時也是字串 \(S\) 長度為 \(nxt_{|S|}\) 的字尾的字尾。又因為字串 \(S\) 長度為 \(nxt_{|S|}\) 的前後綴相等,所以這個字串實際上同時是字串 \(S\) 長度為 \(nxt_{|S|}\) 的字首的前後綴,即字串 \(S\) 長度為 \(nxt_{nxt_{|S|}}\) 的字首。我們從 \(nxt_n\) 開始不斷記錄下當前遍歷到的值並沿著 \(nxt\) 回溯即可。注意 \(n\) 也滿足條件,要加入答案中。

參考程式碼

#include <cstdio>
#include <cstring>
using namespace std;
 
const int maxn = 4e5 + 5;
 
int n;
int nxt[maxn], ans[maxn];
char s[maxn];
 
int main()
{
    while (scanf("%s", s) != EOF)
    {
        int j = 0;
        n = strlen(s);
        memset(nxt, 0, (n + 1) * sizeof(int));
        nxt[0] = nxt[1] = 0;
        for (int i = 1; i < n; i++)
        {
            while (j && s[i] != s[j])
                j = nxt[j];
            if (s[i] == s[j])
                nxt[i + 1] = ++j;
            else
                nxt[i + 1] = 0;
        }
        ans[0] = 0;
        int cur = nxt[n];
        while (cur)
        {
            ans[++ans[0]] = cur;
            cur = nxt[cur];
        }
        for (int i = ans[0]; i >= 1; i--)
            printf("%d ", ans[i]);
        printf("%d\n", n);
    }
    return 0;
}

\(\texttt{T4}\) [中山市選 \(2009\)] 樹

題目大意

給您一棵包含 \(n\) 個結點 \(n - 1\) 條邊的樹,初始時所有結點的點權為 \(0\)。已知每次操作您可以選出一個結點 \(x\),將結點 \(x\) 和與其直接相連的結點點權異或 \(1\)。試求令所有點權變成 \(1\) 至少需要的操作次數。

\(1 \leq n \leq 100\)

解題思路

觀察題目發現需要求最值,優先考慮樹形 \(dp\) 或者樹上貪心。換根 \(dp\) 顯然無法很好地維護答案,因此題目的正解大概率是普通的樹形 \(dp\)。不妨設 \(f_i\) 表示令結點 \(i\) 的子樹點權都變成 \(1\) 所需要的最小的操作次數。此時決策為是否操作結點 \(i\),若不操作,我們需要選出奇數個子結點進行操作來令結點 \(i\) 的點權變成 \(1\)。顯然我們僅維護 \(f_i\) 無法知道最優解是否操作結點 \(i\),因此考慮優化狀態。

我們不妨另設 \(dp_{i, 0}\) 表示令結點 \(i\) 的子樹點權均為 \(1\) 並且不操作結點 \(i\) 的最小操作次數,\(dp_{i, 1}\) 表示令結點 \(i\) 的子樹點權均為 \(1\) 並且操作結點 \(i\) 的最小操作次數。假設 \(u\)\(i\) 子結點,此時 \(dp_{i, 0}\) 可以由奇數個和最小的 \(f_{u, 1}\) 加上其餘的 \(f_{u, 0}\) 來更新。但是對於選中結點 \(i\) 的情況,我們需要討論 \(i\) 的子結點的點權。因此我們還需要進一步優化狀態。

既然我們需要知道結點 \(i\) 的點權以及結點 \(i\) 是否被選中,那麼我們不妨在以下所有狀態均滿足結點 \(i\) 的子樹中除結點 \(i\) 外其餘點權均為 \(1\) 的前提下,另設:

  1. \(dp_{i, 0}\) 表示不操作結點 \(i\) 並且結點 \(i\) 的點權為 \(1\) 的最小操作次數。

  2. \(dp_{i, 1}\) 表示操作結點 \(i\) 並且結點 \(i\) 的點權為 \(1\) 的最小操作次數。

  3. \(dp_{i, 2}\) 表示不操作結點 \(i\) 並且結點 \(i\) 的點權為 \(0\) 的最小操作次數。

  4. \(dp_{i, 3}\) 表示操作結點 \(i\) 並且結點 \(i\) 的點權為 \(0\) 的最小操作次數。

狀態轉移方程大力分類討論即可,總時間複雜度為 \(\mathcal{O}(n)\),詳見程式碼。

參考程式碼

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 1e2 + 5;
const int maxm = 2e2 + 5;
const int inf = 0x3f3f3f3f;

struct node
{
	int to, nxt;
} edge[maxm];

int n, cnt;
int head[maxn], dp[maxn][4];

void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

// dp[u][0] -> 不操作且點權為 1 
// dp[u][1] -> 操作且點權為 1 
// dp[u][2] -> 不操作且點權為 0 
// dp[u][3] -> 操作且點權為 0 
 
void dfs(int u, int fa)
{
	int a, b, c, d;
	// 邊界條件 
	dp[u][0] = dp[u][3] = inf;
	dp[u][1] = 1, dp[u][2] = 0;
	for (int i = head[u]; i; i = edge[i].nxt)
	{
		int v = edge[i].to;
		if (v == fa)
			continue;
		dfs(v, u);
		a = min(inf, min(dp[u][0] + dp[v][0], dp[u][2] + dp[v][1])); //ok
		b = min(inf, min(dp[u][1] + dp[v][2], dp[u][3] + dp[v][3])); //
		c = min(inf, min(dp[u][2] + dp[v][0], dp[u][0] + dp[v][1]));
		d = min(inf, min(dp[u][1] + dp[v][3], dp[u][3] + dp[v][2]));
		dp[u][0] = a, dp[u][1] = b, dp[u][2] = c, dp[u][3] = d;
	}
}

int main()
{
	int u, v;
	while (scanf("%d", &n) && n)
	{
		memset(head, 0, (n + 1) * sizeof(int));
		cnt = 0;
		for (int i = 1; i <= n - 1; i++)
		{
			scanf("%d%d", &u, &v);
			add_edge(u, v);
			add_edge(v, u);
		}
		dfs(1, 0);
		printf("%d\n", min(dp[1][0], dp[1][1]));
	}
	return 0;
}