1. 程式人生 > 其它 >2021牛客多校第五場 題解

2021牛客多校第五場 題解

比賽連結

H題 Holding Two(構造,找規律)

給定正整數 \(n,m\),要求構造一個 \(n\)\(m\) 列的矩陣 \(A\),元素為0或者1,保證同一行的連續三個元素,同一列的連續三個元素,對角線上的連續三個元素,都不可能相等。

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

觀察樣例,推一推,不難構造出這樣一種矩陣:

1100110011001100....
0011001100110011....
1100110011001100....
0011001100110011....
....

程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int n, m;
const char output[2][4] = {{'0', '0', '1', '1'}, {'1', '1', '0', '0'}};
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < m; ++j)
            putchar(output[i % 2][j % 4]);
        puts("");
    }
    return 0;
}

K題 King of Range (ST表/單調佇列,雙指標)

給定一個長度為 \(n\) 的數列 \(\{a_n\}\),以及 \(m\) 次詢問。

每次詢問給出一個整數 \(k\),問一共存在多少組不同區間,使得區間的最大值減去最小值大於 \(k\)

\(1\leq n \leq 10^5,1\leq m \leq 200, 1\leq a_i,k \leq 10^9\)

我們可以先 \(O(n \log n)\) 建一個 ST 表,這樣就可以 \(O(1)\) 查詢每個區間了(我第一遍用了一個黑心模板,每次查詢是 \(O(\log n)\) 的,害得我超時一次,逆天)。

這題看起來好像沒法離線,所以我們必須線上處理,考慮到 \(m\)

的範圍,我們不難猜測正解裡的每次詢問複雜度是 \(O(n)\) 的,而序列上的 \(O(n)\) 演算法,很難不想到雙指標。

顯然,倘若區間(l,r)滿足要求,那麼任何包含該區間的區間同樣符合要求,因為擴充套件區間只會使得最大值更大,最小值更小,兩者之差必然更加大於 \(k\) 。那麼我們不妨雙指標進行處理:固定左端點L後,將右端點不斷向右移動,當 \(\operatorname{query}(L,R)>k\) 的時候,給答案加上 \(n-R+1\),隨後將左端點向右移一位,重複該流程。這個方法是 \(O(n)\) 的,最終的總複雜度為 \(O(nm)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m, a[N];
//ST表
int lg[N];
int ST1[N][25], ST2[N][25];
void init() {
    //
    for (int i = 1; i <= n; i++)
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    for (int i = 1; i <= n; i++)
        lg[i]--;
    //
    for (int i = 1; i <= n; ++i)
        ST1[i][0] = ST2[i][0] = a[i];
    for (int k = 1; k <= 20; ++k)
        for (int i = 1; i + (1 << k) - 1 <= n; ++i) {
            ST1[i][k] = max(ST1[i][k - 1], ST1[i + (1 << (k - 1))][k - 1]);
            ST2[i][k] = min(ST2[i][k - 1], ST2[i + (1 << (k - 1))][k - 1]);
        }
}
inline int queryMax(int l, int r) {
    int k = lg[r - l + 1];
    return max(ST1[l][k], ST1[r - (1 << k) + 1][k]);
}
inline int queryMin(int l, int r) {
    int k = lg[r - l + 1];
    return min(ST2[l][k], ST2[r - (1 << k) + 1][k]);
}
//solve
#define LL long long
LL solve(int k) {
    LL ans = 0;
    for (int l = 1, r = 1; l <= n; ++l) {
        bool flag = (queryMax(l, r) - queryMin(l, r) > k);
        while (!flag && r <= n) {
            r++;
            if (r == n + 1) break;
            if (queryMax(l, r) - queryMin(l, r) > k) flag = true;
        }
        if (r == n + 1) break;
        if (flag) ans += n - r + 1;
    }
    return ans;
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    init();
    while (m--) {
        int k;
        scanf("%d", &k);
        printf("%lld\n", solve(k));
    }
    return 0;
}

此外,我們也可以用單調佇列來維護區間極值,但是並不如 ST 表那麼直觀好寫(主要是我不是很會寫),而且這並不能降低本題的複雜度,所以就不貼程式碼了。

B題 Boxes (數學:概率)

給定 \(n\) 個盒子,每個盒子裡面有一個白球或者黑球,概率均為 \(\dfrac{1}{2}\)。開啟第 \(i\) 個盒子需要 \(w_i\) 的代價。

我們隨時可以花費 \(C\) 的代價開掛,知道此時場上還沒開的盒子裡面有幾個黑球。

問採取最優策略的情況下,知道每個盒子裝著什麼球所花最小代價的數學期望。

\(1\leq n\leq 10^5,0\leq C,w_i\leq 10^9\)

我們不難想到,採取最優策略,必然是先開代價小的後開代價大的,所以我們將所有盒子按照 \(w_i\) 從小到大進行排序,複雜度為 \(O(n\log n)\)

我們記 \(P(k)\) 為前 \(k\) 位並非完全相同,但後 \(n-k\) 位完全相同的情況(注意,這裡要發現第 \(k\) 位和第 \(k+1\) 位要不相同),推出正常狀態下 \(P(k)=(\dfrac{1}{2})^{n-k}\)。特別的,\(P(0)=(\dfrac{1}{2})^{n-1}\)

得到概率公式之後,我們推出答案公式:

\[ans=\min\{C+\sum\limits_{k=0}^{n-1}P(k)*sum(1,k), sum(1,n)\} \]

\(sum(1,k)\) 是開啟前 \(k\) 個盒子的代價)

做一次字首和後線性遞推即可,複雜度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
double C, w[N], dp[N];
int main()
{
    //read
    scanf("%d%lf", &n, &C);
    for (int i = 1; i <= n; ++i)
        scanf("%lf", &w[i]);
    //sort & pre-sum
    sort(w + 1, w + n + 1);
    for (int i = 1; i <= n; ++i) w[i] += w[i - 1];
    //dp
    dp[n] = 1.0;
    for (int i = n - 1; i > 0; i--)
        dp[i] = dp[i + 1] / 2;
    dp[0] = dp[1];
    //solve
    double ans = 0;
    for (int k = 0; k < n; ++k)
        ans += dp[k] * w[k];
    printf("%lf\n", min(ans + C, w[n]));
    return 0;
}

D題 Double Strings (線性DP,組合數)

給定兩個字串 \(A,B\),要求我們從中各選出一個子串,分別記為 \(s,t(s\in A,t\in B)\),且滿足:

  1. \(|s|=|t|=n\)\(n\) 是一個正整數
  2. \(\exists i \in [1,n],s_i<t_i\)
  3. \(\forall j \in[1,i),s_j=t_j\)

求出不同組合 \((s,t)\) 的總個數(這裡的不同,是指字元對應的下標不同)。

\(1\leq|A|,|B|\leq 5000\),兩個字串均只包含小寫字母

簡單來說,這兩個字串存在某一位具有大小關係,前面部分完全相同,後面部分隨便即可。

求公共字首數量可以預處理,是一個典型的二維 DP,記 \(f_{i,j}\)\(A\) 的前 \(i\) 位, \(B\) 的前 \(j\) 位所能構成的公共子序列的數量,有如下 DP 方程,邊界條件為 \(f_{0,*}=f_{*,0}=0\),複雜度 \(O(n^2)\)

\[f_{i,j}=(f_{i-1,j}+f_{i,j-1}-f_{i-1,j-1})+\begin{cases}f_{i-1,j-1}+1, & A_i=B_j \\ 0,& A_i\not=B_j\end{cases} \]

值得一提的是,本題允許這個字首是空的,所以統計答案的時候,還要對每個 \(f_{i,j}\) 加上一才對。

後面的就是排列組合算了,假設後面 \(A\) 還剩下 \(x\) 位,\(B\) 還剩下 \(y\) 位,那麼後面的可能的字尾長度為

\[g=\sum\limits_{i=0}^{\min(x,y)}\C_x^i*\C_y^i \]

我們假設 \(x\leq y\),根據組合的對稱性,可得

\[g=\sum\limits_{i=0}^{x}\C_x^{x-i}*\C_y^i=\C_x^x\C_y^0+\C_x^{x-1}\C_y^1+\cdots+\C_x^0\C_y^x \]

這東西如果在高中做,我感覺我想的可能會快一點:二項式定理!

\((1+a)^x=C_x^0a^0+C_x^1a^1+\cdots+C_x^xa^x,(1+a)^y=C_y^0a^0+C_y^1a^1+\cdots+C_y^ya^y\),那麼便有

\[(1+a)^x(1+a)^y=\sum\limits_{i=0}^{x+y}\sum\limits_{j=0}^{i}\C_x^j\C_y^{i-j}a^i \]

(附:當 \(m>n\) 時,記 \(\C_n^m=0\)

也就是說,\(a^x\) 的係數恰好就是 \(g\)。又因為 \((1+a)^x(1+a)^y=(1+a)^{x+y}\),所以我們得到

\[g=\sum\limits_{i=0}^{x}\C_x^{x-i}*\C_y^i=\C_x^x\C_y^0+\C_x^{x-1}\C_y^1+\cdots+\C_x^0\C_y^x=\C_{x+y}^x \]

我們也先 \(O(n^2)\) 的預處理出所有的組合數即可。

考慮到這個資料規模,不太好用楊輝三角來預處理組合數,所以我們必須用別的方法來 \(O(1)\) 的處理它。因為需要對分數取模,所以顯然是要預處理所有階乘的乘法逆元(\(\dfrac{a}{b}\mod p=a*\operatorname{inv}(b)\mod p\))。

根據費馬小定理,可知 \(\operatorname{inv}(a)=a^{p-2}\mod p\),所以我們可以先算出 \(n!\) 的逆元。

隨後,我們便可以線性遞推出其他階乘的逆元:

\[n!*inv(n!)\equiv1 \bmod p\\ (n-1)!*inv((n - 1)!)\equiv1 \bmod p\\ n!*inv((n - 1)!)\equiv n \bmod p\\ n!*inv(n!)*inv((n - 1)!)\equiv n*inv(n!) \bmod p\\ inv((n - 1)!)\equiv n*inv(n!) \bmod p \]

然後我們便可以 \(O(1)\) 的計算組合數了:

\[\C_n^m=n!*inv(m!)*inv((n-m)!)\bmod p \]

處理好了所有的字首和字尾,我們便可以 \(O(n^2)\) 的列舉中間斷點,隨後 \(O(1)\) 的查詢所有的字首和字尾,並計入答案即可。本題雖然要算的東西賊多,但是最終的總複雜度還是 \(O(n^2)\) 的。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 5010;
const LL mod = 1e9 + 7;
//逆元法快速求組合數
LL fact[N << 1], inv[N << 1];
LL quickpow(LL a, LL b) {
    LL res = 1;
    while (b) {
        if (b & 1) res = res * a % mod;
        b >>= 1;
        a = a * a % mod;
    }
    return res;
}
void init(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n * 2; ++i)
        fact[i] = fact[i - 1] * i % mod;
    inv[n] = quickpow(fact[n], mod - 2);
    for (int i = 2 * n - 1; i >= 0; --i)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}
LL C(LL n, LL m) {
    if (m > n) return 0;
    return fact[n] * inv[m] % mod * inv[n - m] % mod;
}
//
int n, m;
char s[N], t[N];
LL f[N][N];
//
int main()
{
    scanf("%s%s", s + 1, t + 1);
    n = strlen(s + 1), m = strlen(t + 1);
    init(max(n, m) << 1);
    //DP
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j) {
            f[i][j] = (f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + mod) % mod;
            if (s[i] == t[j])
                f[i][j] = (f[i][j] + f[i - 1][j - 1]  + 1) % mod;
        }
    //clac ans
    LL ans = 0;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            if (s[i] < t[j])
                ans = (ans + (1 + f[i-1][j-1]) * C(n - i + m - j, m - j)) % mod;
    printf("%lld", ans);
    return 0;
}

J題 Jewels (二分圖帶權匹配)

現在有 \(n\) 個氣球,第 \(i\) 個氣球的座標為 \((x_i,y_i,z_i)\)。每個氣球有一個速度 \(v_i\),也就是說,時刻 \(t\) 的時候,第 \(i\) 個氣球的座標為 \((x_i,y_i,z_i+tv_i)\)

我們在點 \((0,0,0)\) 處。每個時刻(從0開始)都可以拿走一個氣球,同時花費 \(d^2\) 的體力(若拿走的這個氣球在 \((a,b,c)\) 處,那麼 \(d^2=a^2+b^2+c^2\))。問我們應該採取怎樣的順序,才能花費最少的體力,拿走所有的氣球,並輸出花費體力的最小值。

\(1\leq n\leq 300,0\leq |x_i|,|y_i|,z_i,v_i\leq 1000\),所有資料均為整數

顯然,每個點的 \(x_i,y_i\) 座標對於答案並無影響,我們只需要正常將其加上去就好了。那麼,我們便可以將這個問題從三維空間系化到了一維的數軸上。

排完序之後,我們需要使 \(\sum\limits_{i=0}^{n-1}{(z_i+i*v_i)^2}\) 最大,也就是使 \(\sum\limits_{i=0}^{n-1}{{v_i}^2i^2+2z_iv_ii}\) 最大,......


以上沒啥子用,下面直接看正解:

顯然,用n次便可以拿走所有的氣球,也就是說,我們需要將這幾個時間和對應拿走的氣球一一對應,我考試的時候想到的是基於臨項交換的貪心什麼什麼的,但是這題讓我屬實開眼界了:構建一個二分圖,左邊是各個時間點,右邊是氣球,兩邊之間的邊權則是對應時間所花的體力(一種基於時間的建模方式)。顯然,這是一個二分圖的帶權匹配問題。(但有一說一,我記得不知道在哪看過,當資料規模在100-300 這一級的時候,要麼是 Floyd 或者各種 DP,要麼就是網路流/二分圖了)

因為這顯然是一個完備匹配,而且圖十分稠密,所以可以使用 KM 演算法來完成。(這題資料賊逆天,費用流和基於 DFS 的 KM 演算法全部都 TLE 了,必須得用 BFS 優化過的 KM 演算法,或者用其他奇奇怪怪方法對普通KM進行優化,才能完成這題)

#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 310;
#define INF ((1LL) << 62)
int n;
LL dis[N][N];
//KM
int vis[N], match[N], pre[N];
LL la[N], ra[N], slack[N];
void bfs(LL u) {
    LL x, y = 0, yy = 0, delta;
    memset(pre, 0, sizeof(pre));
    for (int i = 1; i <= n; i++)
        slack[i] = INF;
    match[y] = u;
    while (true) {
        x = match[y], delta = INF, vis[y] = 1;
        for (int i = 1; i <= n; i++) {
            if (vis[i]) continue;
            if (slack[i] > la[x] + ra[i] - dis[x][i]) {
                slack[i] = la[x] + ra[i] - dis[x][i];
                pre[i] = y;
            }
            if (slack[i] < delta)
                delta = slack[i], yy = i;
        }
        for (int i = 0; i <= n; i++)
            if (vis[i]) {
                la[match[i]] -= delta;
                ra[i] += delta;
            }
            else slack[i] -= delta;
        y = yy;
        if (match[y] == -1) break;
    }
    while (y) {
        match[y] = match[pre[y]];
        y = pre[y];
    }
}
LL KM()
{
    memset(match, -1, sizeof(match));
    memset(la, 0, sizeof(la));
    memset(ra, 0, sizeof(ra));
    for (int i = 1; i <= n; i++) {
        memset(vis, 0, sizeof(vis));
        bfs(i);
    }
    LL ans = 0;
    for (int i = 1; i <= n; i++)
        ans += dis[match[i]][i];
    return -ans;
}
int main()
{
    //read & build
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        LL x, y, z, v;
        scanf("%lld%lld%lld%lld", &x, &y, &z, &v);
        for (int t = 0; t < n; ++t)
            dis[t + 1][i] = -x * x - y * y - (z + t * v) * (z + t * v);
    }
    //solve
    printf("%lld", KM());
    return 0;
}