2021杭電多校第一場 (1) 個人補題記錄


比賽鏈接:Here

1001 - Mod, Or and Everything

簽到,

打表發現與 2的次方相關聯

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int _; for (cin >> _; _--;) {
        ll n; cin >> n;
        if (n == 1)cout << "0\n";
        else {
            int k = log(n) / log(2);
            ll sum = pow(2, k);
            if (sum != n)cout << sum - 1 << "\n";
            else cout << sum / 2 - 1 << "\n";
        }
    }
}

1002 - Rocket land

待補

1003 - Puzzle loop

待補

1004 - Another thief in a Shop

待補

1005 - Minimum spanning tree

這里借用一些官方解法說明:

對於編號為3~n的點,將所有編號為合數的點向其約數連邊,編號為質數的點向2連邊,不難證明這樣形

成的生成樹是最小的。

總的邊權和為(質數的和 *2+合數的和),用歐拉篩預處理前綴和即可。

效率: \(\mathcal{O}(n)\)

const int N = 1e7 + 10;
int cnt, vis[N], prime[N];

void init() {
    for (int i = 2; i < N; i ++ ) {
        if (!vis[i]) prime[cnt ++ ] = i;
        for (int j = 0; prime[j] * i <= N; j ++ ) {
            vis[i * prime[j]] = true;
            if (i % prime[j] == 0) break;
        }
    }
}
int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    init();
    int _; for (cin >> _; _--;) {
        int n; cin >> n;
        ll cnt = 0;
        for (int i = 3 ; i <= n; ++i) {
            if (!vis[i])cnt += 2 * i;
            else cnt += i;
        }
        cout << cnt << "\n";
    }
}

1006 - Xor sum (Good ,字典樹)

題意很簡單:

給定長度為 \(n (\le1e5)\) 的序列,找到最短的連續子序列使得 異或和 不小於 \(k(a_i,k\le2^{30})\)

如果存在多個滿足條件的序列,請輸出左端點最小的連續子序列的左端點和右端點,

如果不存在連續子序列的異或和 不小於 \(k\) 則輸出 -1


思路參考於 聆竹聽風

看到題目第一想法是暴力解決,但注意到 \(n\) 的范圍是 \(n\le1e5\) ,寫BF肯定TLE了,

由於異或滿足以下性質:\(sum[l...r] = sum[1...l - 1]⊕sum[1...r]\)

所以我們可以用前綴和,

\(sum[1...i] = S_i\) ,特別地,定義 \(S_0 = 0\),於是有:

\[sum[l...r] = S_{l - 1}⊕S_r \]

從而可以將原問題轉化為,在 \(S_0,S_1,...,S_n\) 中尋找一對在序列中最短的 \(S_i,S_j(i < j)\) ,滿足 \(S_i ⊕ S_j \ge k\),最終答案即為 \([i + 1,j]\) ( \(i+1\) 是左端點,\(j\) 是右端點)

正如最開始說的一樣,最暴力的想法就是利用兩重循環,第一層循環從 \([1,n]\) 枚舉 \(d\) ,第二層枚舉 \(i(0\le i \le n + 1 - d)\) ,判斷 \(S_i ⊕ S_{i + d - 1} \ge k\) 是否成立,成立則說明答案已經找到,跳出循環,不成立則繼續循環。

// 暴力寫法
int l = -1, r = n;
for (int i = 1; i <= n; ++i) {
    int x = 0;
    for (int j = i; j < n; ++j) {
        x ^= a[j];
        if (j - i >= r - l)break;
        if (x >= k)l = i, r = j;
    }
}
if (l >= 0)cout << l << " " << r << "\n";
else cout << "-1\n";

因為暴力做法時間復雜度為 \(\mathcal{O}(n^2)\),會超時,所以需要優化。不妨考慮枚舉右側端點 \(i\) ,嘗試是否能在區間 \([0,i - 1]\) 上快速找到離 \(i\) 最近的 \(j\) 使得 \(S_i ⊕ S_j \ge k\)

既然是異或問題,一定和二進制相關,而題目給出的范圍是 \(0\le a_i\le 2^{30}\) ,所以 \(S_i\) 也在這個范圍中,說明 \(S_i\) 可以用 \(30\) 位二進制表示,於是 \(S_i\) 可以看成長度為 \(30\) 的 "01"字符串。

故而可以考慮在枚舉 \(i\) 的時候,動態地維護一個包含 \(S_0,S_1,...,S_{i−1}\) 的"01"字典樹,其中深度小的節點存儲高位,深度大的節點存儲低位。字典樹的每個節點附加存儲着這個節點所表示的前綴(從高位開始的"01"串)最后一次在數列 \(S_0,S_1,...,S_{i−1}\) 中出現的位置,沒出現過位置就記成 \(−1\)

然后讓 \(S_i\) 和字典樹中的串進行帶剪枝的逐位異或:

為了方便描述,記 \(S_i\) 中從高位到低位數起第 \(j\) 位(以下簡稱“第 \(j\) 位”)為 \(S_{ij}\)\(k\) 中第 \(j\) 位是 \(k_j\)

假設目前正在考慮第 \(j\) 位的情況,即到達了字典樹的第 \(j−1\) 層(根節點為空前綴,把它當成第 \(0\) 層),考慮往哪個方向上的子樹深入下去(並不是兩個方向上都需要深入,即剪枝)。

  1. \(k_j=1\) ,就要求字典樹中所表示串的\(j\)\(S_{ij}\) 異或的結果也是 \(1\) ,才有可能使得最終異或結果大於等於 \(k\),由於\(S_{ij}⊕1\)\(S_{ij}\) 異或結果是 \(1\),故此時需要往 \(S_{ij}⊕1\) 方向的那個子樹上深入。
  2. \(k_j=0\) ,說明字典樹中所表示串的\(j\)\(S_{ij}\)異或的結果可以是 \(0\),也可以是 \(1\)
    • 若字典樹中所表示串的\(j\)\(S_{ij}\) 異或的結果是 \(1\) ,由於\(S_{ij}⊕1\)\(S_{ij}\) 異或結果是 \(1\),即考慮往 \(S_{ij}⊕1\) 方向,發現此時無需進行后續位的異或,也可知道最終異或結果大於\(k\),故無需往 \(S_{ij}⊕1\) 方向的子樹下深入,直接利用往 \(S_{ij}⊕1\) 方向的節點所附加的信息更新答案(別忘了,每個節點附加記錄了這個節點所代表前綴最后一次在數列 \(S_0,S_1,...,S_{i−1}\) 中出現的位置,這是一種剪枝,也是優化的關鍵)。
    • 若字典樹中所表示串的 \(j\)\(S_{ij}\) 異或 的結果是 \(0\) ,由於 \(S_{ij}\)\(1\) 異或結果是 \(0\) ,即考慮往\(S_{ij}\)方向,此時還需要進行后續位的異或,故需要往 \(S_{ij}\) 方向的子樹上深入。

假如逐位異或能夠進行到最后一位,那說明異或到最后才比較出大於等於 \(k\),此時直接利用葉節點附加信息更新答案。

\(S_i\) 與字典樹中的串進行帶剪枝的逐位異或之后,就需要把 \(S_i\) 這個串插入到字典樹中,注意插入過程需要更新節點的附加信息,以便后續計算。

時間復雜度分析:由於字典樹只會往一個方向遍歷,設整數序列最大的數為 \(P\) (最大為 \(2^{30}\)),則樹的最大深度是\(log⁡P\),整數序列長度為 \(n\),故復雜度為 \(O(nlogP)\),本題中可以認為是 \(O(30n)\)

【AC Code】

const int N = 1e5 + 10, M = 3e6 + 10;
int a[N];
int ch[M][2], val[M];

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int _; for (cin >> _; _--;) {
        int n, k, tot = 1;
        cin >> n >> k;
        a[0] = 0;
        for (int i = 1; i <= n; ++i) {
            cin >> a[i];
            a[i] ^= a[i - 1];
        }

        ch[1][0] = ch[1][1] = 0;
        val[1] = 1;
        int l = -1, r = n + 1;
        for (int i = 0; i <= n; ++i) {
            int now = 1, ans = -1;
            for (int j = 29; j >= 0; --j) {
                int dig = (a[i] >> j) & 1;
                if ((k >> j) & 1)           // k的當前位為1,只能和dig異或結果為1,才可能大於等於k
                    now = ch[now][dig ^ 1]; // 與dig異或結果為1的數是dig^1
                else {                      // k的當前位為0,和dig異或結果可以是1也可以是0
                    if (ch[now][dig ^ 1])   // 和dig異或結果為1,后面的位都無須看,結果一定大於k
                        ans = max(ans, val[ch[now][dig ^ 1]]);
                    // 和dig異或結果是1的情況就無須遍歷,只需要遍歷和dig異或結果為0的情況
                    now = ch[now][dig];
                }
                // 節點沒了
                if (now == 0) break;
            }

            if (now) ans = max(ans, val[now]);
            // 更新當前最小區間序列
            if (ans >= 0 and i - ans < r - l) {
                l = ans, r = i;
            }
            now = 1;
            for (int j = 29; j >= 0; --j) {
                int dig = (a[i] >> j) & 1;
                if (!ch[now][dig]) {
                    ch[now][dig] = ++tot;
                    ch[tot][0] = ch[tot][1] = 0;
                    val[tot] = -1;
                }
                now = ch[now][dig];
                val[now] = max(val[now], i);
            }
        }

        if (l == -1 and r == n + 1) cout << "-1\n";
        else cout << l + 1 << " " << r << "\n";
    }
}

順便貼一下官方解釋:

對數列做前綴異或,將題面轉化為找兩個距離最近的數,使得他們的異或值不小於 \(k\)

枚舉靠右的那個數,同時維護字母樹,字母樹每個節點保存范圍內最靠右的點的位置。根據k來詢

問對應的 \(log\) 個節點,從而更新答案。

效率: \(O(nlogn)\)

1007 - Pass!

待補

1008 - Maximal submatrix (Good)

題意:

給定一個數字矩陣,求出最大面積的滿足每列非遞減的矩陣


一開始dp寫的,但發現TLE了,然后轉念想到這完全可以用單調棧解決。在思考的同時隊友WJX大牛已經AC了,所以比賽的時候就沒往下想。賽后補一下

本題正解為原矩陣轉為01矩陣,再使用懸線法求最大01矩陣即可

01 矩陣:1 代表該位置是否比前一位小

懸線法算法講解:Here

復雜度 \(O(n^2)\)

const int N = 2e3 + 10;
// a為原矩陣,b轉為01矩陣
int a[N][N], b[N][N];
int H[N], Q[N];

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int _; for (cin >> _; _--;) {
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                cin >> a[i][j];
                b[i][j] = 0; // init
                if (i > 1)b[i][j] = (a[i][j] >= a[i - 1][j]);
            }

        for (int i = 1; i <= m; ++i) H[i] = 0;

        int ans = 0;
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                if (b[i][j] == 0) H[j] = 1;
                else H[j]++;
            }
            int cnt = 0;
            H[m + 1] = 0;
            for (int j = 1; j <= m + 1; ++j) {
                while (cnt and H[Q[cnt]] > H[j]) {
                    ans = max(ans, (j - Q[cnt - 1] - 1) * H[Q[cnt]]);
                    --cnt;
                }
                Q[++cnt] = j;
            }
        }
        cout << ans << '\n';
    }
}

在貼一下 WJX 大牛的單調棧寫法:

const int N = 2010;
int c[N][N];
int n, m, h[N][N];

LL work(int h[N], int n) {
    int l[N], r[N], q[N];
    h[0] = h[n + 1] = -1;
    int tt = 0;
    q[0] = 0;
    for (int i = 1; i <= n; i++) {
        while (h[i] <= h[q[tt]]) tt--;
        l[i] = q[tt];
        q[++tt] = i;
    }
    tt = 0;
    q[0] = n + 1;
    for (int i = n; i >= 1; i--) {
        while (h[i] <= h[q[tt]]) tt--;
        r[i] = q[tt];
        q[++tt] = i;
    }
    LL ans = 0;
    for (int i = 1; i <= n; i++) {
        ans = max(ans, (LL)h[i] * (r[i] - l[i] - 1));
    }
    return ans;
}

int main() {
    ios::sync_with_stdio(false);
    int t;
    cin >> t;
    while (t--) {
        memset(c, 0, sizeof(c));
        memset(h, 0, sizeof(h));
        cin >> n >> m;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                cin >> c[i][j];
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                if (c[i][j] >= c[i - 1][j]) h[i][j] = h[i - 1][j] + 1;
                else h[i][j] = 1;
        LL ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = max(ans, work(h[i], m));
        }
        cout << ans << endl;
    }
    return 0;
}

1009 -KD-Graph (Good)

思維+並查集,簽到(霧

題意都能看懂就不放了


將邊按權值從小到大排序,每一階段取出同權值的所有邊,將這些邊的端點用並查集兩兩合並,若某一階段的全部邊合並完,並查集數量為k,則當前階段合並邊的權值就是答案,否則輸出-1。

const int N = 1e6 + 10;
struct node {int u, v, w;} q[N];
int f[N];
int find(int x) {return f[x] == x ? x : f[x] = find(f[x]);}
bool cmp(node a, node b) {return a.w < b.w;}
int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    int _; for (cin >> _; _--;) {
        int n, m, k;
        cin >> n >> m >> k;
        int now = n, ans = 0;
        for (int i = 1; i <= n; i++) f[i] = i;
        for (int i = 1; i <= m; i++) cin >> q[i].u >> q[i].v >> q[i].w;
        sort(q + 1, q + 1 + n, cmp);
        for (int i = 1 ; i <= m; ++i) {
            if (q[i].w != q[i - 1].w) {if (now == k) break;}
            if (find(q[i].u) == find(q[i].v))continue;
            now --, f[find(q[i].u)] = find(q[i].v), ans = q[i].w;
        }
        cout << (now == k ? ans : - 1) << "\n";
    }
}

1010 - zoto

待補

1011 - Necklace of Beads

看完題目感覺能做,但證明寫了半個小時硬是沒想出來,淦,太菜了

這里貼一下正解證明過程

1011-證明1

1011-證明2

【AC Code】

using ll = long long;
const int N = 1000001, mod = 998244353;
ll n, k, ans, fac[N], inv[N], invfac[N], f[N], p[N], vis[N], prime[N], cnt, phi[N];
void init() {
    phi[1] = p[0] = fac[0] = invfac[0] = inv[1] = fac[1] = invfac[1] = 1;
    p[1] = 2;
    for (ll i = 2; i < N; i++) {
        if (!vis[i])prime[++cnt] = i, phi[i] = i - 1;
        for (ll j = 1; j <= cnt && i * prime[j] < N; j++) {
            vis[i * prime[j]] = 1;
            if (i % prime[j] == 0) {
                phi[i * prime[j]] = phi[i] * prime[j];
                break;
            }
            phi[i * prime[j]] = phi[i] * (prime[j] - 1);
        }
        p[i] = p[i - 1] * 2ll % mod;
        fac[i] = fac[i - 1] * i % mod;
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        invfac[i] = invfac[i - 1] * inv[i] % mod;
    }
}

ll C(ll n, ll m) {
    if (n < 0 || m < 0 || m > n)return 0;
    return fac[n] * invfac[m] % mod * invfac[n - m] % mod;
}

ll get(ll n, ll k) {
    f[0] = n & 1 ? 0 : 2;
    for (ll m = 1; m <= n; m++)
        f[m] = (p[m] * (C(n - m, m) + C(n - m - 1, m - 1)) + f[m - 1]) % mod;
    return f[min(n, k)];
}

int main() {
    cin.tie(nullptr)->sync_with_stdio(false);
    init();
    int _; for (cin >> _; _--;) {
        ans = 0;
        cin >> n >> k;
        for (ll d = n; d >= 1; d--)
            if (n % d == 0)
                ans = (ans + phi[n / d] * get(d, k * d / n)) % mod;
        cout << ans *inv[n] % mod << "\n";
    }
}


免責聲明!

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



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