2024百度之星決賽部分題解(難度排序前六題)

TJUHuangTao發表於2024-08-18

前言

手速6題,可惜第四題磨了幾個小時沒磨出來,多做一題就金了,還是實力差了點,最後銀牌前列。下面的題解是根據程式碼回憶大概的題意,主要是給出來賽時的參考程式碼

A.狀壓

題意:

學校集訓隊總的有 \(n\) 個人,保證 \(n\) 是3的倍數,每個人有個人實力 \(a_i\), 每兩個人之間有配合程度 \(b_{ij}\),每個人可以選擇掛機或者不掛機,隊伍有不同的總權值(題目中把所有可能的搭配都給出來了), 問合理安排下,最多使得所有隊伍的權值和最大是多少。

分析

看到資料範圍很顯然是考慮狀壓DP,二進位制狀態1表示已經安排好隊伍,0表示還沒,從小到大列舉狀態時候,一次性列舉三個是0的位置,考慮這三個人的最大權值,然後更新dp值。
然後可以稍微預處理一下,有效的狀態一定是1的數量是3的倍數的,可以把這些數提前存vector裡面,可以少列舉一些狀態,而且 \(C(n, 3)\)列舉時候可以先把狀態裡為0的位置存起來,再3個for迴圈列舉,也能最佳化一點點。

程式碼

點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = (1 << 22) + 100;
int a[maxn], b[30][30];
int dp[maxn];
int co[30][30][30];
vector<int> vec;
int cal(int x, int y, int z) {
    int res = max({a[x], a[y], a[z], b[x][y], b[y][z], b[x][z],
                   b[x][y] * b[y][z] * b[x][z]});
    return res;
}
signed main() {
    int n;
    cin >> n;
    for (int i = 0; i < (1 << n); i++)
        if (__builtin_popcount(i) % 3 == 0)
            vec.push_back(i);
    for (int i = 0; i < (1 << n); i++)
        dp[i] = -1e18;
    dp[0] = 0;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> b[i][j];
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            for (int k = 0; k < n; k++)
                co[i][j][k] = cal(i, j, k);
    for (auto u : vec) {
        vector<int> tem;
        for (int i = 0; i < n; i++)
            if (!(u >> i & 1))
                tem.push_back(i);
        int sz = tem.size();
        for (int i = 0; i < sz; i++)
            for (int j = i + 1; j < sz; j++)
                for (int k = j + 1; k < sz; k++) {
                    int nxt = u | (1 << tem[i]) | (1 << tem[j]) | (1 << tem[k]);
                    dp[nxt] = max(dp[nxt], dp[u] + co[tem[i]][tem[j]][tem[k]]);
                }
    }
    cout << dp[(1 << n) - 1] << "\n";
    return 0;
}

B.模擬、高精

題意

給定一個n \((n \leq 21)\) 位的二進位制數,輸出它和十進位制數21相乘後的結果的二進位制表示

分析

由於乘法不用管進位制,所以直接類似高精度乘法一樣,對這兩個二進位制數乘法,並且21比較小已經給定,甚至可以直接取21的二進位制為1的那些位,相當於把原來二進位制串右移對應位後相加,資料範圍比較小,所以隨便怎麼暴力都可以。

程式碼

點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 1e5 + 10;
int a[maxn], b[maxn], c[maxn];
signed main() {
    string str;
    cin >> str;
    reverse(all(str));
    for (int i = 0; i < str.length(); i++)
        a[i] = str[i] - '0';
    int tem = 21, cnt = 0;
    while (tem) {
        b[cnt++] = tem % 2;
        tem /= 2;
    }
    for (int i = 0; i < 5; i++) {
        if (b[i]) {
            for (int j = 0; j < str.length(); j++) {
                c[j + i] += a[j];
            }
        }
    }
    int mx = str.length() + 30;
    for (int i = 0; i <= mx; i++) {
        c[i + 1] += c[i] / 2;
        c[i] %= 2;
    }
    string ans;
    for (int i = 0; i <= mx + 31; i++)
        ans.push_back(c[i] + '0');
    while (!ans.empty() && ans.back() == '0')
        ans.pop_back();
    reverse(all(ans));
    cout << ans << "\n";
    return 0;
}

C. 01BFS

題意

有一個二維無限平面,給定起始點 \((x1, y1)\) 和終點 \((x2, y2)\), 平面上有 \(n\) 個障礙物,它們的座標都是 \(0 \leq x \leq 10^3\), \(0 \leq y \leq 10^3\),問從起點到終點,最少要穿過幾個障礙物

分析

這題應該有更簡單的方法,但是賽時比較緊張,第一反應是建一個最短路,相鄰格點建邊,目標有冰塊就權值1,否則權值0,然後跑迪傑斯特拉。不過發現評測機跑樣例都記憶體超限了,就被迫考慮最佳化,發現剛好權值都是0和1,想到01bfs,就不用建邊,直接跑最短路,在佇列取出來的時候再判斷相鄰節點有哪些,然後權值是1的放隊尾,權值0的放隊首。賽後想想,可能普通的最短路也行,只要不顯式的建邊,可能也不會爆記憶體。
這題有個坑點,就是建邊時候判斷邊界要是1001,不能只判斷到1000,因為雖然那些座標都在1000以內,但是實際人是可以繞到1000外面再走回去,所以得多往外面判斷幾格。

點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 1e3 + 10;
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
int tag[maxn][maxn], dis[maxn * maxn], vis[maxn * maxn];
int trans(int i, int j) {
    return (i - 1) * 1005+ j;
}
signed main() {
    memset(dis, 0x3f, sizeof dis);
    int n, x1, y1, x2, y2;
    cin >> n >> x1 >> y1 >> x2 >> y2;
    int m = 1e3 + 5;
    for (int i = 1; i <= n; i++) {
        int x, y;
        cin >> x >> y;
        tag[x][y] = 1;
    }
    deque<int> dq;
    dis[trans(x1, y1)] = 0;
    dq.push_back(trans(x1, y1));
    while (!dq.empty()) {
        int u = dq.front();
        dq.pop_front();
        if (vis[u]) continue;
        vis[u] = 1;
        int x = (u - 1) / m + 1, y = u % m;
        if (x == x2 && y == y2) {
            cout << dis[u] << "\n";
            return 0;
        }
        for (int i = 0; i < 4; i++) {
            int xx = x + dx[i], yy = y + dy[i];
            if (xx < 1 || xx > m || yy < 1 || yy > m) continue;
            int v = trans(xx, yy);
            dis[v] = min(dis[v], dis[u] + tag[xx][yy]);
            if (tag[xx][yy]) dq.push_back(v);
            else dq.push_front(v);
        }
    }
    return 0;
}

5.二分、倍增

這題我感覺是前六題裡面最難的,一開始還讀錯題了

題意

原題意有點繞,我直接給出形式化題意吧。給定一個陣列, 將其分成不超過 \(k\) 個連續子陣列, 每一段內的極差 (最大值減最小值)不超過 \(d\)。然後問三個問題:
假如只給定 \(k\) 求最小的 \(d\), 假如只給定 \(d\) 求最小的 \(k\), 假如給定了 \(d\)\(k\) ,問最長能從原陣列裡取多長的子陣列。

分析

  • 第一個問題,給定分段數,求最小極差,顯然可以二分答案,檢驗方式也很明顯,假設當前二分的答案是mid,從頭到尾遍歷陣列,維護當前分段的最大最小值,如果超了就另起一段。看最後分段的數量有沒有超過 \(k\)
  • 第二個問題,給定極差,求最小分段數。發現這不就是第一個問題裡的check函式,所以從頭掃一遍陣列,維護極差,超了另起一段,最後總的段數就是答案。
  • 第三個問題比較難一點,給定 \(d\)\(k\) ,求最長的連續子陣列滿足這兩個條件的長度。會發現,假如選定了這個子陣列的起始位置,然後就是之前的check函式那樣的暴力判斷,看什麼時候不滿足這兩個條件,複雜度 \(O(n^2)\)。然後想到,每個位置,往後一直到不合法為止,這個長度是固定的,相當於是一段一段的跳。所以可以預處理,從每個位置開始,往後第一個不合法的地方,列舉起點後就可以連著跳 \(k\) 次,看最後到了哪裡。然後發現這個過程可以用倍增維護,直接預處理每個位置往後跳 \(2^i\) 個段的位置,就可以列舉起點後 \(logk\)的判斷終點。然後怎麼判斷第一次跳到哪裡會超,可以用st表加二分,看第一次最大值和最小值差超過 \(d\) 的位置就是所求的,然後倍增處理查詢即可。

程式碼

點選檢視程式碼
#include <bits/stdc++.h>
// #define int long long
#define pii pair<int, int>
#define ll long long
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 5e5 + 10;
int arr[maxn], lg[maxn];
int mx[maxn][22], mn[maxn][22];
int st[maxn][22];
int n, k, d;
int qmn(int l, int r) {
    int k = lg[r - l + 1];
    return min(mn[l][k], mn[r - (1 << k) + 1][k]);
}
int qmx(int l, int r) {
    int k = lg[r - l + 1];
    return max(mx[l][k], mx[r - (1 << k) + 1][k]);
}
bool ck1(int mid) {
    // 分成 k 段, 求最小 段內極差
    int cnt = 1;
    int mxx = arr[1], mnn = arr[1];
    for (int i = 1; i <= n; i++) {
        mxx = max(mxx, arr[i]);
        mnn = min(mnn, arr[i]);
        if (mxx - mnn > mid) {
            cnt++;
            mxx = arr[i];
            mnn = arr[i];
        }
        if (cnt > k) return 0;
    }
    return cnt <= k;
}
signed main() {
    for (int i = 2; i < maxn; i++)
        lg[i] = lg[i >> 1] + 1;
    cin >> n >> k >> d;
    for (int i = 1; i <= n; i++)
        cin >> arr[i], mn[i][0] = mx[i][0] = arr[i];
    for (int j = 1; j < 21; j++)
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            mn[i][j] = min(mn[i][j - 1], mn[i + (1 << (j - 1))][j - 1]);
            mx[i][j] = max(mx[i][j - 1], mx[i + (1 << (j - 1))][j - 1]);
        }
    {
        int l = 0, r = 1e9, ans = 0;
        while (l <= r) {
            int mid = ((ll)l + r) >> 1;
            if (ck1(mid)) ans = mid, r = mid - 1;
            else l = mid + 1;
        }
        cout << ans << "\n";
    }
    {
        //給定極差 d 求最小段數
        int mxx = arr[1], mnn = arr[1], ans = 1;
        for (int i = 1; i <= n; i++) {
            mxx = max(mxx, arr[i]);
            mnn = min(mnn, arr[i]);
            if (mxx - mnn > d) {
                mxx = arr[i], mnn = arr[i];
                ans++;
            }
        }
        cout << ans << "\n";
    }
    {
        // 預處理每個位置開始往後跳第一次越界的地方
        for (int i = 1; i <= n; i++) {
            int l = i + 1, r = n, ans = n + 1;
            while (l <= r) {
                int mid = ((ll)l + r) >> 1;
                int mxx = qmx(i, mid), mnn = qmn(i, mid);
                if (mxx - mnn > d) ans = mid, r = mid - 1;
                else l = mid + 1; 
            }
            st[i][0] = ans;
        }
        st[n + 1][0] = n + 1;
        for (int j = 1; j < 21; j++)
            for (int i = 1; i <= n + 1; i++)
                st[i][j] = st[st[i][j - 1]][j - 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) { //列舉起點
            int s = i;
            for (int j = 0; j < 21; j++)
                if (k >> j & 1)
                    s = st[s][j];
            ans = max(ans, s - i);
            if (n - i + 1 <= ans)
                break;
        }
        cout << ans << "\n";
    }
    return 0;
}

6. DP,LIS

題意

給定長度為 \(n\) (\(n\)) 的陣列,問有多少個子序列是嚴格山峰的(有且僅有一個最大值,左邊嚴格遞增,右邊是嚴格遞減)

分析

考慮去列舉山峰的最大值,以它為最大值的貢獻,是左側的嚴格上升子序列,並且結尾小於當前最大值。右側的嚴格下降子序列,並且開頭小於中間這個最大值的總和,二者乘積即為當前的貢獻。所以就去處理左右兩側,然後就轉化為,對每個 \(i\) 求以它結尾的最長上升子序列個數,用樹狀陣列即可處理,是經典dp。再倒著做一遍,然後前字尾兩個樹狀陣列,不斷維護一下前字尾的那些子陣列數量就好。

程式碼

點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 3e5 + 10;
struct BIT {
    vector<int> v;
    int len;
    int lowbit(int x) { return x & -x; }
    BIT(int n) {
        len = n + 3;
        v.resize(len);
    }
    void update(int i, int x) {
        i++;
        for (int pos = i; pos <= len; pos += lowbit(pos))
            v[pos] = ((v[pos] + x) % mod + mod) % mod;
    }
    int ask(int i) {
        int res = 0;
        i++;
        for (int pos = i; pos; pos -= lowbit(pos))
            res = ((res + v[pos]) % mod + mod) % mod;
        return res;
    }
    void clear() {
        for (int i = 0; i < len; i++)
            v[i] = 0;
    }
};
int arr[maxn], b[maxn];
int predp[maxn], sufdp[maxn];
signed main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
        b[i] = arr[i];
    }
    sort(b + 1, b + 1 + n);
    int len = unique(b + 1, b + 1 + n) - b - 1;
    for (int i = 1; i <= n; i++) {
        arr[i] = lower_bound(b + 1, b + 1 + len, arr[i]) - b;
    }
    BIT pre(len), suf(len);
    pre.update(0, 1);
    for (int i = 1; i <= n; i++) {
        predp[i] = pre.ask(arr[i] - 1);
        pre.update(arr[i], predp[i]);
    }
    suf.update(0, 1);
    for (int i = n; i >= 1; i--) {
        sufdp[i] = suf.ask(arr[i] - 1);
        suf.update(arr[i], sufdp[i]);
    }
    pre.clear();
    pre.update(0, 1);
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        suf.update(arr[i], -sufdp[i]);
        ans = ((ans + pre.ask(arr[i] - 1) * suf.ask(arr[i] - 1) % mod) % mod + mod) % mod;
        pre.update(arr[i], predp[i]);
    }
    cout << ans << "\n";
    return 0;
}
## 7.LCA、邊差分、堆 ### 題意 給一棵樹,每條邊有邊權,然後給一個序列,從1號節點出發按順序經過這些節點。總權值定義為經過的所有的邊權的和。然後至多有 $k$ $(k \leq 10^9)$次操作,每次可以選擇一條邊權 $w$ 使其變為 $\lfloor \frac{w}{2} \rfloor$, 問最少的總權值和是多少。 ### 分析 首先,很容易想到使用邊差分(先把路徑的權值跟節點繫結,然後一條路徑u->v 對應u的次數++, v的次數++, lca的次數-=2,最後一次dfs累加差分陣列還原)來統計出,最終按照這個序列走完後,樹上的每條邊被經過的次數。然後題意轉化為有若干條邊,每條邊有選擇次數 $cnt$ 和權值 $w$, 最多進行 $k$ 次操作,每次讓一個邊的 $cnt$ 除以2,求 $min (\sum cnt \times w)$ 這個地方,很容易考慮去貪心的用一個堆,每次找 $cnt \times w$ 最大的邊,然後除以2,放回去。不過看到資料範圍 $k \leq 10^9$ 會以為超時。不過仔細分析一下就發現,由於對於 $cnt$ 最多除以log次就變成0了,而變成0後這條邊就肯定不會再用到,直接pop。所以有效的除以2次數也就 $nlogn$ 次,所以直接暴力的這樣貪心刪就可以 $nlog(n^2)$ 的得到答案。 實現上,由於有除以二向下取整這個操作,所以保險起見可以直接新建一個結構體存 $cnt$ 和 $w$,然後過載運算子,把小於的規則定義為 $w$ 除以二向下取整後,總權值的變化量小於。 ### 程式碼
點選檢視程式碼
#include <bits/stdc++.h>
#define int long long
#define pii pair<int, int>
#define db double
#define tii tuple<int, int, int>
#define all(a) a.begin(), a.end()
using namespace std;
const int mod = 998244353;
const int maxn = 6e5 + 10;
vector<pii> G[maxn];
int lg[maxn], fa[maxn][23], dep[maxn];
int xu[maxn], val[maxn], sub[maxn];
struct Node {
    int a, b;
    Node(int a1, int b1) {a = a1, b = b1;}
};
void dfs(int u, int f, int deep) {
    fa[u][0] = f, dep[u] = deep;
    for (int i = 1; i <= lg[dep[u]]; i++)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for (auto [v, w] : G[u]) {
        if (v == f) continue;
        val[v] = w;
        dfs(v, u, deep + 1);
    }
}
int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    while (dep[u] > dep[v])
        u = fa[u][lg[dep[u] - dep[v]]];
    if (u == v) return u;
    for (int i = lg[dep[u]]; ~i; i--)
        if (fa[u][i] != fa[v][i])
            u = fa[u][i], v = fa[v][i];
    return fa[u][0];
}
void dfs1(int u, int f) {
    for (auto[v, w] : G[u]) {
        if (v == f) continue;
        dfs1(v, u);
        sub[u] += sub[v];
    }
}
bool operator < (Node a, Node b) {
    auto [x, y] = a;
    int res1 = x * y - (x / 2) * y;
    auto [c, d] = b;
    int res2 = c * d - (c / 2) * d;
    return res1 < res2;
}
signed main() {
    for (int i = 2; i < maxn; i++)
        lg[i] = lg[i >> 1] + 1;
    int n, m, k;
    cin >> n >> m >> k;
    for (int i = 1; i < n; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        G[u].emplace_back(v, w);
        G[v].emplace_back(u, w);
    }
    for (int i = 1; i <= m; i++)
        cin >> xu[i];
    xu[0] = 1;
    dfs(1, 0, 0);
    for (int i = 1; i <= m; i++) {
        int u = xu[i - 1], v = xu[i];
        sub[u]++, sub[v]++;
        sub[lca(u, v)] -= 2;
    }
    dfs1(1, 0);
    priority_queue<Node> q;
    for (int i = 2; i <= n; i++) {
        q.push(Node(val[i], sub[i]));
    }
    while (k-- && !q.empty()) {
        auto [w, cnt] = q.top();
        q.pop();
        w /= 2;
        if (cnt)
            q.push(Node(w, cnt));
    }
    int ans = 0;
    while (!q.empty()) {
        auto [w, cnt] = q.top();
        q.pop();
        ans += cnt * w;
    }
    cout << ans << "\n";
    return 0;
}

後記

第四題不會做可惜了,這裡放個題意,感興趣的可以想想。給一個長度為 \(n\) 的環形字串,每個位置有一個權值。然後多次操作,第一種操作是修改單點權值。第二種操作是選擇一個區間,然後從左到右開始,每有兩個同樣的字元,就刪去這兩個字元以及中間的字元。求最後剩下的字元的位置上的權值和。
再練一年希望明年拿個金,加訓!

相關文章