1. 程式人生 > 其它 >[考試總結]ZROI-21-NOIP10連-DAY1 總結

[考試總結]ZROI-21-NOIP10連-DAY1 總結

ZROI-21-NOIP10連-DAY1 總結

被完虐... QwQ

#T1

#Problem

給定 \(n,p\),求:

\[\sum\limits_{i=1}^n\sum\limits_{j=1}^p\varphi(i^j)\mod 10^9+7. \]

#Solution

#60pts

考場上最初以為是莫比烏斯反演的題,先簡單推一推:

\[\begin{aligned} &\sum\limits_{i=1}^n\sum\limits_{j=1}^p\varphi(i^j)=\sum\limits_{i=1}^n\sum\limits_{j=1}^p\sum\limits_{d|i^j}\mu(d)\dfrac{i^j}{d}\\ =&\sum\limits_{d=1}^n\mu(d)\sum\limits_{i=1}^{\left\lfloor\frac{n}{d}\right\rfloor}\sum\limits_{j=1}^pi^jd^{j-1}=\sum\limits_{j=1}^p\sum\limits_{d=1}^n\mu(d)d^{j-1}\sum\limits_{i=1}^{\left\lfloor\frac{n}{d}\right\rfloor}i^j \end{aligned} \]

推到這裡,我已經做不下去了,這樣的時空複雜度可以做到 \(O(np)\)

,可得 \(60pts\),注意到,這裡無法繼續進行的原因是冪沒有合適的處理方法。

#100pts

接著上面的錯誤思路返回原點,突然想起尤拉函式具有以下性質:

\[\varphi(i^j)=\varphi(i)\cdot i^{j-1} \]

於是原式化為

\[\begin{aligned} &\sum\limits_{i=1}^n\sum\limits_{j=1}^p\varphi(i^j)=\sum\limits_{i=1}^n\sum\limits_{j=1}^pi^{j-1}\varphi(i)=\sum\limits_{i=1}^n\varphi(i)\sum\limits_{j=1}^pi^{j-1}\\ =&p\cdot\varphi(1)+\sum\limits_{i=2}^n\varphi(i)\dfrac{i^p-1}{i-1}=p\cdot\varphi(1)+\sum\limits_{i=2}^n\dfrac{1}{i-1}\varphi(i)(i^p-1)\\ =&p\cdot\varphi(1)+\sum\limits_{i=2}^n\dfrac{1}{i-1}\left(\varphi(i)\cdot i^p-\varphi(i)\right) \end{aligned} \]

對於後面的和式,不難發現 \(\varphi\)

積性函式\(i^p\)完全積性函式,於是我們便可以用線性篩篩出這兩個函式,其中在 \(i\) 為質數時,\(i^p\) 需要使用一次快速冪,而小於等於 \(n\) 的質數個數 \(\pi(n)\) 約為 \(\dfrac{n}{\ln n}\),於是線性篩的整體時間複雜度約為 \(O(n\log_np)\),可以接受。

我們再線性推個逆元,直接求和即得答案。

#Code

#60pts

#include <bits/stdc++.h>
#define ll long long
#define int long long
#define mset(l, x) memset(l, x, sizeof(l))
using namespace std;

const int M = 10010;
const int N = 10000010;
const ll MOD = 1e9 + 7;
const int INF = 0x3fffffff;

int n, p, nprm[N], prm[N], pcnt;
ll mi[M][M], mu[N], sum[M][M], ans, sum1[N];

inline ll fpow(ll a, ll b) {
    ll res = 1;
    while (b) {
        if (b & 1) (res *= a) %= MOD;
        (a *= a) %= MOD; b >>= 1;
    }
    return res;
}

void euler(int x) {
    mu[1] = 1;
    for (int i = 2; i <= x; i ++) {
        if (!nprm[i]) prm[++ pcnt] = i, mu[i] = -1;
        for (int j = 1; j <= pcnt; j ++) {
            if (prm[j] * i > x) break;
            nprm[prm[j] * i] = true;
            if (i % prm[j]) mu[i * prm[j]] = -mu[i];
            else break;
        }
    }
}

signed main() {
    scanf("%lld%lld", &n, &p); euler(n);
    if (p == 1) {
        for (int i = 1; i <= n; ++ i)
          sum1[i] = (sum1[i - 1] + i) % MOD;
        for (int d = 1; d <= n; ++ d) {
            ans += mu[d] * sum1[n / d] % MOD;
            (ans += MOD) %= MOD;
        }
          
    } else {
        for (int i = 1; i <= n; ++ i)
          for (int j = 0; j <= p; ++ j) {
            mi[i][j] = fpow(i, j) % MOD;
            sum[i][j] = (sum[i - 1][j] + mi[i][j]) % MOD;
          }
        for (int i = 1; i <= p; ++ i)
          for (int d = 1; d <= n; ++ d) {
              ans += mu[d] * mi[d][i - 1] % MOD * sum[n / d][i] % MOD;
              (ans += MOD) %= MOD; 
          }
    }
    printf("%lld", ans);
    return 0;
}

#100pts

#include <bits/stdc++.h>
#define ll long long
#define mset(l, x) memset(l, x, sizeof(l))
using namespace std;

const int M = 10010;
const int N = 10000010;
const ll MOD = 1e9 + 7;
const int INF = 0x3fffffff;

ll n, p, mu[N], nprm[N], prm[N], pcnt;
ll ans, phi[N], f[N], mi[N], inv[N];

inline ll fpow(ll a, int b) {
    ll res = 1;
    while (b) {
        if (b & 1) (res *= a) %= MOD;
        (a *= a) %= MOD; b >>= 1;
    }
    return res;
}

void euler(int x) {
    phi[1] = 1; f[1] = 1;
    for (int i = 2; i <= x; i ++) {
        if (!nprm[i]) {
            prm[++ pcnt] = i, mi[i] = fpow(i, p);
            phi[i] = i - 1, f[i] = phi[i] * mi[i] % MOD;
        }
        for (int j = 1; j <= pcnt; j ++) {
            if (prm[j] * i > x) break;
            nprm[prm[j] * i] = true;
            if (i % prm[j]) {
                f[i * prm[j]] = f[i] * f[prm[j]] % MOD;
                phi[i * prm[j]] = phi[i] * (prm[j] - 1) % MOD;
            } else {
                phi[i * prm[j]] = phi[i] * prm[j] % MOD;
                f[i * prm[j]] = f[i] * prm[j] % MOD * mi[prm[j]] % MOD;
                break;
            }
        }
    }
}

int main() {
    scanf("%lld%lld", &n, &p);
    euler(n); inv[0] = inv[1] = 1;
    for (int i = 2; i <= n; ++ i) 
      inv[i] = ((MOD - MOD / i) * inv[MOD % i]) % MOD;
    for (int i = 2; i <= n; ++ i)
      (ans += inv[i - 1] * (f[i] - phi[i]) % MOD + MOD) %= MOD;
    printf("%lld", (ans + p % MOD + MOD) % MOD);
    return 0;
}

#T2

巨大詐騙題,賽時想麻煩了(

#Problem

一個含有 \(n\) 個點的大根堆是一個二叉樹,左右兒子有區別,每個節點上有一個 \(1\sim n\) 的值,任意兩個節點上的值不同,且父親節點的權值大於子節點的權值。

現在給定一個集合 \(S\),要求 \(\forall a\in S\),值 \(a\) 對應的節點是葉結點。求有多少個滿足要求的大根堆,答案 \(\bmod 10^9+7\)

#Solution

首先明確概念:堆不一定是完全二叉樹!

“左偏堆是不是堆?左偏堆是完全二叉樹嗎?” ——wzy

因為是一個大根堆,於是插入順序可以直接確定為由大到小插入,我們設當前可選位置個數為 \(s\),不難發現,對於一個必須為葉子的節點,不論放到哪裡,下一個點的可選位置一定減少一,如果不是,那麼自己佔了一個位置,又提供了兩個可選位置,於是下一個點的可選位置一定增加一,將每一步的 \(s\) 相乘即得答案。

#Code

#include <bits/stdc++.h>
#define ll long long
#define mset(l, x) memset(l, x, sizeof(l))
using namespace std;

const int N = 1000010;
const ll MOD = 1e9 + 7;
const int INF = 0x3fffffff;

int n,  p, vis[N];
ll s = 1, ans = 1;

int main() {
    scanf("%d%d", &n, &p);
    for (int i = 1; i <= p; ++ i) {
        int x; scanf("%d", &x); vis[x] = 1;
    }
    for (int i = n; i >= 1; -- i) {
        (ans *= s) %= MOD;
        if (vis[i]) -- s; else ++ s;
    }
    printf("%lld", ans);
    return 0;
}

#T3

#Problem

初始有一張含有 \(n\) 個點的有向圖,你可以不斷向其中加入至多 \(n(n−1)\) 條邊,不允許加入重邊或者自環。

定義一個長度為 \(m\) 的序列 \(\{ai\}\) 為一個 SCC 序列,當且僅當存在一種加邊方案,使得加入 \(i\) 條邊後圖中恰好存在 \(a_i\) 個強連通分量(兩個點在一個強連通分量中當且僅當他們能夠相互到達)。

給定 \(n\),你需要求出當序列長度分別為 \(1\cdots n(n−1)\) 時,有多少種 SCC 序列,由於答案可能很大,需要對於一個數字取模。為了防止打表,模數是輸入的。

#Solution

用的 zyc 大佬的思路,這裡說的詳細一點。

我們考慮這個序列字典序最大是多少,假設 \(n=4\),那麼這個序列是:

\[4,4,4,4,4,4,3,2,2,1,1,1 \]

記該序列為 \(\{mxl_i\}\) 我們考慮這個序列是怎麼形成的,一開始有 \(n\) 個空點,然後相鄰連邊,就可以使 scc 個數不變。加的不能加為止,不難發現,每多連一條邊,就可以有 \(x\) 條邊使得 scc 個數為 \(n−x\) 不變。這個自己畫一下圖就能論證。

接下來我們考慮如果已知一個字首 scc 序列,如何看下一個值最小能填多少?我們不難發現,這個數是 \(n−k+1\),其中 \(n\) 是序列長度,\(k\) 是序列中不同的數的個數。這個很好論證,用構造的方法就可以證明。

至於上界,這個就是上一位和這一位的 \(mxl\) 值做 \(\min\) 就可以。

上下界固定後,暴力就好想了,DP 也很自然。

\(f_{i,j,k}\) 表示序列長度為 \(i\),最後一個數為 \(j\),有 \(k\) 個不同的數的方案數。

我們考慮有哪些狀態可以轉移到 \(f_{i,j,k}\)。我們考慮如果這個序列中 \(j\) 不止一個,那麼就可以從 \(f_{i−1,j,k}\) 中轉移。

否則,我們列舉第 \(i−1\) 個填了什麼,然後轉移,不難發現這個東西是 \(f_{i−1,j',k−1}\) 其中 \(j'>j\),因為不合法的狀態我們已經設定為 \(0\),所以可以放心轉移。用字首和優化,可以做到 O(Tn^4)。

空間簡單卡一下可以通過,保險起見,這裡採用滾動陣列優化。

#Code

#include <bits/stdc++.h>
#define ll long long
#define mset(l, x) memset(l, x, sizeof(l))
using namespace std;

const int N = 101;
const int INF = 0x3fffffff;

int f[2][N][N], pre[N][N], MOD, mxl[N * N];

inline void prework(int n) {
    for (int i = 1; i <= n * (n - 1) / 2; ++ i) mxl[i] = n;
    for (int i = n - 1; i; -- i)
      for (int j = (n - i) * (n - i - 1) / 2 + 1; j <= (n - i) * (n - i - 1) / 2 + n - i; ++ j)
        mxl[n * (n - 1) / 2 + j] = i;
}

inline int red(int x) {return x >= MOD ? x - MOD : x;}

inline void solve(int n) {
    mset(f, 0); f[1][n][1] = 1; printf("1 ");
    for (int i = 2; i <= n * (n - 1); ++ i) {
        mset(f[i & 1], 0); int ans = 0;
        for (int j = 1; j <= n && j <= i; ++ j) {
            pre[1][j] = f[!(i & 1)][1][j];
            for (int k = 2; k <= mxl[i - 1]; ++ k)
              pre[k][j] = red(pre[k - 1][j] + f[!(i & 1)][k][j]);
        }
        for (int j = 1; j <= mxl[i]; ++ j)
          for (int k = 1; k <= n && k <= i && k <= (n - j + 1); ++ k) {
              int l = max(j, n - i + k - 1); if (l > j) continue; 
              f[i & 1][j][k] = (pre[mxl[i - 1]][k - 1] - pre[l + (l == j) - 1][k - 1] + MOD) % MOD;
              if (l == j) f[i & 1][j][k] = red(f[i & 1][j][k] + f[!(i & 1)][j][k]);
              ans = red(ans + f[i & 1][j][k]);
          }
        printf("%d ", ans);
    } printf("\n");
}

int main() {
    int T, n; scanf("%d", &T);
    while (T --) {
        scanf("%d%d", &n, &MOD);
        prework(n); solve(n);
    }
    return 0;
}

#T4

#Problem

有一個長度為 \(n\) 的數列 \(a_i\),Alice 和 Bob 手上分別有一個初始為 \(0\) 的數字(分別記為 \(A,B\))。Alice 和 Bob 輪流做如下操作:

  • 從序列開頭或者結尾取出一個數 \(x\),讓自己手上的數字異或上 \(x\),並把 \(x\) 從序列中刪除。

Alice 先手,最後誰手上的數字大誰就勝了。如果 Alice,Bob 均使用最優策略,那麼誰能取勝?(或者平局,即兩個人手上的數字一樣大)

#Solution

首先不難發現,當 \(a_i\) 的異或和為 \(0\) 時一定為平局,否則 \(s\) 在二進位制下最高的非零位 \(p\) 必然是決勝的關鍵,於是我們便可以將原本的 \(a_i\) 按照第 \(p\) 位是否為 \(1\) 轉變為 01 序列,下面的操作都是在 01 序列上進行的討論。

分為兩種情況:長度為奇數和長度為偶數。

先來看第一種:長度為偶數。我們將 01 序列分為奇數位偶數位,此時 a_i 的異或和不為 \(0\),所以必然要麼是奇數位上有奇數個 \(1\),要麼是偶數位上有奇數個 \(1\),於是先手可以直接控制後手所選的 \(1\) 的奇偶性,故在此種情況下先手必勝。

再來看長度為奇數的情況:

  • 假如兩端都是 \(0\),那麼無論先手選哪個都會使當前後手變為長度為偶數的先手狀態,於是此時先手必敗;

  • 假如一端為 \(1\),那麼先手必然要選作為 \(1\) 的一端,否則必敗,不是最優策略;之後先手需要跟隨後手的選擇,保證兩者的選擇完全一致,否則必敗我們來簡單討論一下原因:

    • 假如在這一步之前,所有的先手的每一步都跟隨著後手選,所以此時先後手選的 \(1\) 的數量的奇偶性一定不同,在這一步時,後手選了 \(1\),先手選了 \(0\),那麼此時先後手選的 \(1\) 的數量的奇偶性變為相同,於是一共選了偶數個 \(1\),還剩奇數個 \(1\),此時一共選了奇數個數字,於是還剩偶數個數字,此時後手作為當前局面的先手進入必勝狀態;於是這樣選先手必敗。
    • 在這一步時,後手選了 \(0\),先手選了 \(1\),與上面的證明同理,可得這樣先手必敗。

    由於先手取走了一個 \(1\),那麼此時 \(1\) 的個數為偶數,由於每一步先手都跟著後手選,所以先後手必然平分偶數個 \(1\),如果這個個數不能被 \(4\) 整除,也就意味著先手被分到了奇數個 \(1\),加上了最初的那個 \(1\),必敗。

那麼,怎樣的序列才能滿足先手能跟著後周的每一步選呢?刪除掉左右兩邊相同的數後,剩下的應當滿足相鄰奇偶位(如 \(1\)\(2\) 是,但 \(2\)\(3\) 不是)上的數字相同,這裡不證正確性,證法與上面相似。

#Code

#include <bits/stdc++.h>
#define ll long long
#define mset(l, x) memset(l, x, sizeof(l))
using namespace std;

const int N = 200010;
const int INF = 0x3fffffff;

int a[N], s, n, T, l, r, p, tot;

inline bool check(int l, int r, int cnt) {
    while (l < r && a[l] == a[r]) ++ l, -- r;
    while (l < r && a[l] == a[l + 1]) l += 2;
    if (l < r) return 0; else return 1;
}

int main() {
    scanf("%d", &T);
    while (T --) {
        scanf("%d", &n); l = 1, r = n;
        p = 30, tot = 0, s = 0;
        for (int i = 1; i <= n; ++ i)
          scanf("%d", &a[i]), s ^= a[i];
        if (!s) {printf("Draw\n"); continue;}
        if (!(n & 1)) {printf("Alice\n"); continue;}
        while (p && !((s >> p) & 1)) -- p;
        for (int i = 1; i <= n; ++ i)
          a[i] = (a[i] >> p) & 1, tot += a[i];
        if (a[1] == 0 && a[n] == 0) {printf("Bob\n"); continue;}
        -- tot; if (tot % 4) {printf("Bob\n"); continue;}
        if ((a[1] && check(2, n, tot)) || (a[n] && check(1, n - 1, tot)))
          printf("Alice\n");
        else printf("Bob\n");
    }
    return 0;
}

期望得分:\(100+20+0+0=120\)
實際得分:\(100+30+0+0=130\)
沒想到還騙過了一個點(