比賽鏈接: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\),於是有:
從而可以將原問題轉化為,在 \(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\) 層),考慮往哪個方向上的子樹深入下去(並不是兩個方向上都需要深入,即剪枝)。
- 當 \(k_j=1\) ,就要求字典樹中所表示串的第 \(j\) 位和 \(S_{ij}\) 異或的結果也是 \(1\) ,才有可能使得最終異或結果大於等於 \(k\),由於\(S_{ij}⊕1\) 與 \(S_{ij}\) 異或結果是 \(1\),故此時需要往 \(S_{ij}⊕1\) 方向的那個子樹上深入。
- 當\(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}\)),則樹的最大深度是\(logP\),整數序列長度為 \(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
看完題目感覺能做,但證明寫了半個小時硬是沒想出來,淦,太菜了
這里貼一下正解證明過程
【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";
}
}