[考試總結]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)\)
#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\)
我們再線性推個逆元,直接求和即得答案。
#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\)
沒想到還騙過了一個點(