一些有趣的問題合集
這些問題大多數出自於做過的模擬題,大多數在OJ上都找不到。
我會不定期更新這個合集。
kand
Problem Descript
給定$n$個數,從中選出$k$個數,問使得它們and和為$s$的方案數。
Input
第一行三個正整數$n$, $k$和$s$。
接下來有$n$行,每行一個整數$a_{i}$表示第$i$個數。
Output
僅一行,一個整數,表示答案對$10^{9} + 7$取模後的結果。
Sample
input
3 2 2
3
2
1output
1
Limit
對於10%的資料滿足$1\leqslant n \leqslant 10, 0 \leqslant a_{i}, s \leqslant 2^{10}$
對於30%的資料滿足$1\leqslant n \leqslant 100, 0 \leqslant a_{i}, s \leqslant 2^{10}$
對於70%的資料滿足$1\leqslant n \leqslant 10^{5}, 0 \leqslant a_{i}, s \leqslant 2^{15}$
對於所有的資料滿足$1\leqslant n \leqslant 10^{5}, 0 \leqslant a_{i}, s \leqslant 2^{20}$Source
idy002
Solution 1
暴力列舉$k$組合。期望得分10分。
Solution 2
用$f[i][j][k]$表示考慮到前$i$個數中,選出$j$個數,它們的and和為$k$的方案數。
時間複雜度$O(n^{2}2^{W})$,期望得分30分。
Solution 3
當首先過濾掉$s\ and\ a_{i} \neq s$的數。
當$s$的某一位為0時,那麼選出的數至少有一個這一位為0,那麼可以考慮容斥。
用所有方案減去存在不合法的方案。
不合法的方案可以列舉每次選出來的數哪幾位為1,然後暴力計算$cnt$,用組合數算方案。
時間複雜度$O(3^{W} + n)$,期望得分70分。
Solution 4
發現暴力求$cnt$的過程是FWT的正變換。
於是FWT優化一下就好了。
時間複雜度$O(W\cdot 2^{W} + n)$,期望得分100分。
Code
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 typedef boolkandboolean; 5 6 const int Val = (1 << 20), mask = Val - 1, M = 1e9 + 7; 7 8 void exgcd(int a, int b, int& d, int& x, int& y) { 9 if (!b) 10 d = a, x = 1, y = 0; 11 else { 12 exgcd(b, a % b, d, y, x); 13 y -= (a / b) * x; 14 } 15 } 16 17 int inv(int a, int n) { 18 int d, x, y; 19 exgcd(a, n, d, x, y); 20 return (x < 0) ? (x + n) : (x); 21 } 22 23 int n, k, s; 24 int *jc, *ijc; 25 int cnt[Val]; 26 27 inline void init() { 28 scanf("%d%d%d", &n, &k, &s); 29 jc = new int[(n + 1)]; 30 ijc = new int[(n + 1)]; 31 int nc = 0, S = s ^ mask; 32 for (int i = 1, x; i <= n; i++) { 33 scanf("%d", &x); 34 if ((s & x) == s) 35 cnt[x & S]++, nc++; 36 } 37 n = nc; 38 } 39 40 inline void prepare() { 41 jc[0] = 1; 42 for (int i = 1; i <= n; i++) 43 jc[i] = jc[i - 1] * 1ll * i % M; 44 ijc[n] = inv(jc[n], M); 45 for (int i = n - 1; ~i; i--) 46 ijc[i] = ijc[i + 1] * 1ll * (i + 1) % M; 47 } 48 49 inline int C(int n, int k) { 50 if (n < k) return 0; 51 return (jc[n] * 1ll * ijc[k]) % M * ijc[n - k] % M; 52 } 53 54 inline void solve() { 55 int ans = C(n, k), S; 56 for (int i = 0; i < 20; i++) { 57 S = mask ^ (1 << i); 58 for (int j = S; j; j = (j - 1) & S) 59 cnt[j] += cnt[j | (1 << i)]; 60 } 61 S = s ^ mask; 62 for (int i = S, sign, j; i; i = (i - 1) & S) { 63 if (cnt[i]) { 64 for (j = i, sign = 1; j; j -= j & (-j), sign *= -1); 65 ans = (ans + sign * C(cnt[i], k)) % M; 66 if (ans < 0) ans += M; 67 } 68 } 69 printf("%d\n", ans); 70 } 71 72 int main() { 73 freopen("kand.in", "r", stdin); 74 freopen("kand.out", "w", stdout); 75 init(); 76 prepare(); 77 solve(); 78 return 0; 79 }
青蛙
Solution 1
直接暴力列舉所有情況。
時間複雜度$O(2^{m})$。期望得分20分。
Solution 2
設當前考慮第$i$只青蛙跳躍。
那麼這次後,它結束的位置的期望是$x_{i}' = \frac{x_{i} - 2(x_{i} - x_{i - 1}) + x_{i} - 2(x_{i} - x_{i + 1})}{2}=x_{i + 1} + x_{i - 1} - x_{i}$。
然後暴力遞推。時間複雜度$O(mk)$。期望得分40分。
Solution 3
暴力打表可以發現在$n, m \geqslant 10^{3}$時,隨即資料下,操作的迴圈節大約在$5000$左右。
然後找一下迴圈節,就可以騙到70分。
Solution 4
可以發現一個有趣的事情:
$x_{i}' - x_{i - 1}= x_{i + 1} - x_{i}$
$x_{i + 1} - x_{i}' = x_{i} - x_{i - 1}$
這相當於一次操作交換了相鄰的兩個差。
這個可以看成一個置換。可以處理出一輪操作的置換,最終的差分陣列等於先把這個置換進行$k$次冪,然後作用在原來的差分陣列上。
由於置換的合成是$O(n)$的,所以可以直接快速冪,時間複雜度$O(n\log k)$
也可以把置換看成輪換,把中間每個環拉出來,然後往前走$k$步,時間複雜度$O(n)$。
期望得分100分。
由於我比較懶,就只給出快速冪的程式碼。
Code
1 #include <iostream> 2 #include <cstring> 3 #include <cstdio> 4 #ifndef WIN32 5 #define Auto "%lld" 6 #else 7 #define Auto "%I64d" 8 #endif 9 using namespace std; 10 11 #define ll long long 12 13 const int N = 1e5 + 5; 14 15 typedef class MyVector { 16 public: 17 int a[N]; 18 19 MyVector() { } 20 MyVector(int n) { 21 for (int i = 1; i <= n; i++) 22 a[i] = i; 23 } 24 MyVector(int* ar, int n) { 25 for (int i = 1; i <= n; i++) 26 a[ar[i]] = i; 27 } 28 29 int& operator [] (int p) { 30 return a[p]; 31 } 32 }MyVector; 33 34 int n, m; 35 ll k; 36 int *ar, *qr; 37 int *da, *nda; 38 39 MyVector operator * (MyVector a, MyVector b) { 40 MyVector rt; 41 for (int i = 1; i < n; i++) 42 rt[i] = a[b[i]]; 43 return rt; 44 } 45 46 MyVector qpow(MyVector& a, ll pos) { 47 MyVector pa = a, rt(n - 1); 48 for ( ; pos; pos >>= 1, pa = pa * pa) 49 if (pos & 1) 50 rt = pa * rt; 51 return rt; 52 } 53 54 inline void init() { 55 scanf("%d", &n); 56 ar = new int[(n + 1)]; 57 qr = new int[(n + 1)]; 58 da = new int[(n + 1)]; 59 nda = new int[(n + 1)]; 60 for (int i = 1; i < n; i++) 61 qr[i] = i; 62 for (int i = 1; i <= n; i++) 63 scanf("%d", ar + i); 64 for (int i = 1; i < n; i++) 65 da[i] = ar[i + 1] - ar[i]; 66 scanf("%d"Auto, &m, &k); 67 for (int i = 1, x; i <= m; i++) { 68 scanf("%d", &x); 69 swap(qr[x - 1], qr[x]); 70 } 71 } 72 73 MyVector a; 74 75 inline void solve() { 76 a = MyVector(qr, n - 1); 77 a = qpow(a, k); 78 for (int i = 1; i < n; i++) 79 nda[a[i]] = da[i]; 80 ll ps = ar[1]; 81 printf(Auto"\n", ps); 82 for (int i = 1; i < n; i++) { 83 ps += nda[i]; 84 printf(Auto"\n", ps); 85 } 86 } 87 88 int main() { 89 freopen("frog.in", "r", stdin); 90 freopen("frog.out", "w", stdout); 91 init(); 92 solve(); 93 return 0; 94 }青蛙
dalao
注:因為樣例2有錯所以被我刪掉了。
Solution 1
暴力列舉每一對$(x, y)$
時間複雜度$O(n^{2})$,期望得分10分。
Solution 2
經典的三維偏序問題。
可以CDQ分治或者樹套樹。
時間複雜度$O(n \log ^{2} n)$,期望得分60 ~ 100分。(某ZJC加上各種毒瘤優化,卡進時限4s,實際上時間限制可以只開2s)。
Solution 3
設$K_{x, y} = [a_{x} < a_{y}] + [b_{x} < b_{y}] + [c_{x} < c_{y}]$
$S_{x, y} = \max \{K_{x, y}, K_{y, x}\}$。
顯然$S_{x, y} \in \{2, 3\}$。
設$A = \sum_{1\leqslant x < y \leqslant n}[S_{x, y} = 3]$, $B = \sum_{1\leqslant x < y \leqslant n}[S_{x, y} = 2]$。
那麼$A$就是問題所要我們求的。
另外,設$P_{a, b} = \sum_{1 \leqslant x, y \leqslant n}[a_{x} < a_{y}][b_{x} < b_{y}]$。
有注意到$A + B = C_{n}^{2}$。然後細心會發現:
$3\times A + B =P_{a, b} + P_{a, c} + P_{b, c}$
因為每一對合法的無序對$(x, y)$都會對右邊的式子作出3次貢獻,不合法的無序對會對右邊的式子作出1次貢獻。
然後根據這兩個式子解解方程就能得到$A$了。
時間複雜度$O(n \log n)$,期望得分100分。
Code
1 #include <iostream> 2 #include <cstdlib> 3 #include <cstring> 4 #include <cstdio> 5 #include <ctime> 6 #ifndef WIN32 7 #define Auto "%lld" 8 #else 9 #define Auto "%I64d" 10 #endif 11 using namespace std; 12 typedef bool boolean; 13 14 #define ll long long 15 16 typedef class IndexedTree { 17 public: 18 int s; 19 int* ar; 20 21 IndexedTree() { } 22 IndexedTree(int n):s(n) { 23 ar = new int[(n + 1)]; 24 memset(ar, 0, sizeof(int) * (n + 1)); 25 } 26 27 void add(int idx, int x) { 28 for ( ; idx <= s; idx += (idx & (-idx))) 29 ar[idx] += x; 30 } 31 32 int getSum(int idx) { 33 int rt = 0; 34 for ( ; idx; idx -= (idx & (-idx))) 35 rt += ar[idx]; 36 return rt; 37 } 38 }IndexedTree; 39 40 int myrand(int& seed) { 41 return seed = ((seed * 19260817ll) ^ 233333) & ((1 << 24) - 1); 42 } 43 44 void gen(int* ar, int s, int n) { 45 for (int i = 1; i <= n; i++) ar[i] = i; 46 for (int i = 1; i <= n; i++) swap(ar[i], ar[myrand(s) % i + 1]); 47 } 48 49 int n; 50 ll res = 0; 51 int *ar, *br, *cr; 52 int *ls; 53 IndexedTree it; 54 55 inline void init() { 56 scanf("%d", &n); 57 int sa, sb, sc; 58 scanf("%d%d%d", &sa, &sb, &sc); 59 ar = new int[(n + 1)]; 60 br = new int[(n + 1)]; 61 cr = new int[(n + 1)]; 62 ls = new int[(n + 1)]; 63 it = IndexedTree(n); 64 gen(ar, sa, n); 65 gen(br, sb, n); 66 gen(cr, sc, n); 67 } 68 69 ll calc(int* ar, int* br) { 70 for (int i = 1; i <= n; i++) 71 ls[ar[i]] = br[i]; 72 ll rt = 0; 73 memset(it.ar, 0, sizeof(int) * (n + 1)); 74 for (int i = 1; i <= n; i++) { 75 rt += it.getSum(ls[i]); 76 it.add(ls[i], 1); 77 } 78 return rt; 79 } 80 81 inline void solve() { 82 res = calc(ar, br); 83 res += calc(ar, cr); 84 res += calc(br, cr); 85 res -= n * 1ll * (n - 1) / 2; 86 res /= 2; 87 printf(Auto, res); 88 } 89 90 int main() { 91 freopen("dalao.in", "r", stdin); 92 freopen("dalao.out", "w", stdout); 93 init(); 94 solve(); 95 return 0; 96 }dalao
區間
Solution 1
找出所有連續區間。然後用dp的思想去更新答案就好了。
時間複雜度$O(n^{2} + m)$,期望得分30分、
Solution 2
由於是一個排列,所以式子可以寫成$max - min = r - l$。考慮暴力找連續區間的過程是列舉$l$,然後再列舉$r$的過程。當$l$不變的時候,$r$遞增,如果$max$和$min$均為改變有一點小浪費。
因此可以考慮將$max$和$min$分段,然後每段每段地跳,每跳一次段可以直接計算出位置,然後判斷一下是否合法。
時間複雜度$O(n^{2} + m)$,期望得分30 ~ 70分、
Solution 3
注意到兩個連續區間如果存在公共部分,那麼將它們合併後也一定是一個連續區間。
因為這是一個排列,所以將公共部分去掉後的兩個區間的值域不會相交。不難證明兩個連續區間合併的值域長度等於新區間的長度。
因此可以考慮CDQ分治。假設當前分治區間$[l, r]$,分治中心$mid$。
我可以列舉一個$l'$然後找到一個最短的包含區間$[l', mid + 1]$的區間。
這個可以用一個st表存值的大小,一個st表存一個值對應的下標。
當要求包含區間$[l, r]$的時候,可以在第一個st表中查詢這一段的最大值和最小值,然後在第二個st表中查詢這一段值域中的每個值在原陣列中出現的下標的最大值r'和最小值l'。也就是意味著要包含區間$[l', r']$,於是便可以這樣迭代去查。終止條件是$l' = l, r' = r$。
不理解st表維護的資訊?那麼換個解釋方法,前者對陣列$a_{i}$的區間最大最小值建立st表,後者對陣列$b_{a_{i}} = i$的區間最大最小值建立st表。
但是這樣做時間複雜度有問題,不可能每個位置都去查詢一次。
可以繼續發現一些優美性質:
性質1 如果存在四個整數滿足$1 \leqslant l' < l \leqslant r' < r \leqslant n$,且使得$[l', r'],[l, r]$是連續的,那麼$[l', r]$也是連續的。
證明 因為這是一個排列,所以非公共部分的值域是沒有交集的。因此公共部分的值域是連續的。所以$[l', r]$也是連續的。
性質2 分治時,當要找到包含區間$[l', mid + 1]$時,且長度儘量小,可選的區間是唯一的。
證明 假設存在兩個候選區間$[a, b],[c,d]$且滿足$b - a = d - c, l\leqslant a < c \leqslant l' < mid + 1 \leqslant b < d \leqslant r$。
那麼根據性質1,可以找到區間$[c, b]$作為答案。顯然$b - c < d - c$,與候選區間要求滿足的性質矛盾。
也許有些人會有點懷疑這個迭代做法的正確性,這個過程中,暫時先不考慮時間複雜度的問題。
演算法的正確性 如果列舉左端點$l'$,那麼找到的合法區間$[a, b]$是長度最小的。
證明 合法性 根據迭代終止條件和迭代過程易證(一個是要求連續,一個是要求包含$[l', mid + 1]$)。
最優性 假設存在一個更優的區間$[c, d]$,根據性質2,我們有$a < c, d \leqslant b$或$a \leqslant c, d < b$,因為要求包含$[l', mid + 1]$,而迭代是先從這個區間開始的,所以必然存在一個區間$[e, f]$恰好被$[c, d]$包含並且迭代的下一個區間沒有被$[c, d]$包含(PS:如果下一個區間恰好為$[c, d]$,易證它不合法)。因此$[e, f]$的最小值和最大值之間每一個數都要在$[c, d]$中出現,但是根據迭代過程可知,必然存在一個$x$,使得它存在於$[e, f]$的最小值和最大值之間,但不在$[c, d]$中,因此$[c, d]$不是一個連續區間,與題目要求矛盾。
所以綜上所述,演算法是正確的。
現在來考慮優化。
性質3 當列舉的左端點單調遞減時,右端點不減。
證明 仍然考慮反證法。假設列舉存在兩個最優區間$[a, b], [c, d]$,滿足$l \leqslant a < c \leqslant b < d \leqslant r$。
根據性質1,$[c, b]$是一個更優的答案。因此$[c, d]$不是最優的區間,矛盾。
有了這種優美的性質,不難得到,每次找到的區間是真包含前一個區間。
所以對於上一個找到的區間$[a, b]$。我只要再將$a + 1$然後繼續用迭代法去找下一個區間就行了。
對於右邊我們仍然做一次類似的過程:列舉$r'$,找到包含區間$[mid, r']$的一些區間。
然後這有什麼用呢?
這個過程找到了一系列左端點遞減的跨過分治中心的合法區間和一系列右端點遞增的跨過分治中心的合法區間。
對於查詢一個區間$[l', r'], l \leqslant l' \leqslant r' \leqslant r$,我在左邊找到一個左端點小於等於$l'$的區間,在右邊找到一個右端點大於等於$r'$的區間。然後將這兩個區間取並集(注意是取並集!)嘗試去更新這個詢問的答案。
這個方法很多,可以二分。由於值域很小,也可以直接求前後綴最值。
時間複雜度$O(n\log n + m\log n)$,期望得分100分。
Code
1 #include <algorithm> 2 #include <iostream> 3 #include <cassert> 4 #include <cstring> 5 #include <cstdio> 6 #include <vector> 7 using namespace std; 8 typedef bool boolean; 9 10 const int N = 1e5 + 5, bzmax = 18; 11 12 #define pii pair<int, int> 13 #define fi first 14 #define sc second 15 16 pii operator + (pii a, pii b) { 17 return pii(min(a.fi, b.fi), max(a.sc, b.sc)); 18 } 19 20 boolean operator < (pii a, pii b) { 21 return a.sc - a.fi < b.sc - b.fi; 22 } 23 24 typedef class SparseTable { 25 public: 26 int log2[N]; 27 pii f[N][18]; 28 29 SparseTable() { } 30 SparseTable(int n, int *ar) { 31 log2[1] = 0; 32 for (int i = 2; i <= n; i++) 33 log2[i] = log2[i >> 1] + 1; 34 for (int i = 1; i < n; i++) 35 f[i][0] = pii(ar[i], ar[i]) + pii(ar[i + 1], ar[i + 1]); 36 for (int j = 1; j < bzmax; j++) 37 for (int i = 1; i + (1 << j) <= n; i++) 38 f[i][j] = f[i][j - 1] + f[i + (1 << (j - 1))][j - 1]; 39 } 40 41 pii query(int a, int b) { 42 assert(a != b); 43 int p = log2[b - a]; 44 return f[a][p] + f[b - (1 << p)][p]; 45 } 46 }SparseTable; 47 48 typedef class Query { 49 public: 50 int l, r; 51 pii res; 52 }Query; 53 54 int n, m; 55 int *ar, *var; 56 Query *qs; 57 pii *ls, *rs; 58 SparseTable st1, st2; 59 60 inline void init() { 61 scanf("%d", &n); 62 ar = new int[(n + 1)]; 63 var = new int[(n + 1)]; 64 for (int i = 1; i <= n; i++) 65 scanf("%d", ar + i); 66 for (int i = 1; i <= n; i++) 67 var[ar[i]] = i; 68 scanf("%d", &m); 69 qs = new Query[(n + 1)]; 70 for (int i = 1; i <= m; i++) 71 scanf("%d%d", &qs[i].l, &qs[i].r), qs[i].res = pii(1, n); 72 } 73 74 void dividing(int l, int r, vector<Query*>& qs) { 75 if (qs.empty()) return; 76 if (l == r) { 77 for (int i = 0; i < (signed) qs.size(); i++) 78 qs[i]->res = pii(l, l); 79 return; 80 } 81 int mid = (l + r) >> 1; 82 vector<Query*> ql, qr; 83 for (int i = 0; i < (signed) qs.size(); i++) 84 if (qs[i]->r <= mid) 85 ql.push_back(qs[i]); 86 else if (qs[i]->l > mid) 87 qr.push_back(qs[i]); 88 89 dividing(l, mid, ql); 90 dividing(mid + 1, r, qr); 91 92 int tl = 0, tr = 0; 93 pii cur, nx, p1, p2; 94 for (nx = cur = pii(mid, mid + 1); cur.fi >= l && cur.sc <= r; cur.fi--) { 95 do { 96 nx = st1.query(cur.fi, cur.sc); 97 nx = st2.query(nx.fi, nx.sc); 98 } while (cur != nx && (cur = nx).fi >= l && cur.sc <= r); 99 if (cur.fi >= l