写在前面: 省赛选手,省银(邀请铜尾),前面猛猛掉分,最后的 G G G 还一直没开出来,图论想到死都没开出来,结果赛后一看是 D P DP D P 省赛牛魔不配有和牌的合照
补题连接
# 题目大意算 ( a + b + c ) ∗ d (a+b+c) * d ( a + b + c ) ∗ d
# 题解
C++ 1 2 3 4 5 void solve () { int a, b, c, d; cin >> a >> b >> c >> d; cout << (a + b + c) * d << endl; }
# 题目大意现在有一个长方体,两个顶点分别位于 ( 0 , 0 , 0 ) (0,0,0) ( 0 , 0 , 0 ) 和 ( a , b , c ) (a,b,c) ( a , b , c ) 有 n n n 藤蔓,分别从 ( x 1 , y 1 , z 1 ) (x_1,y_1,z_1) ( x 1 , y 1 , z 1 ) 到 ( x 2 , y 2 , z 2 ) (x_2,y_2,z_2) ( x 2 , y 2 , z 2 ) 你现在可以发动一攻击,任意从垂直 x , y , z x,y,z x , y , z 轴的任意位置发动,切断与这一个截面相交的所有藤蔓 请问你最多切断多少条藤蔓? (只需要考虑长方体内即可)
# 数据范围1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1 ≤ n ≤ 1 0 5 1 ≤ a , b , c ≤ 1 0 9 1 \le a,b,c \le 10^9 1 ≤ a , b , c ≤ 1 0 9 # 题解这题其实可以考虑成,在 x , y , z x,y,z x , y , z 三个方向上的, 最多重复字段数
,取最大即可
# 写法 1 - 贪心这是我们队赛时的写法,利用贪心 + 优先队列,跑三遍最多重叠的区间数量 注意要按左端点排序( while
条件里是 heap.top() < v[i].first
)
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 int n, a, b, c;bool cmp (PII x, PII y) { return x.first < y.first; } int getRes (vector<PII>& v) { sort (v.begin (), v.end (), cmp); int res = 0 ; priority_queue<int , vector<int >, greater<int >> heap; for (int i = 0 ; i < v.size (); i++) { int t = 0 ; while (heap.size () && heap.top () < v[i].first) heap.pop (); heap.push (v[i].second); res = max (res, int (heap.size ())); } return res; } void solve () { cin >> n >> a >> b >> c; vector<PII> x, y, z; for (int i = 1 ; i <= n; i++) { int x1, y1, z1, x2, y2, z2; cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2; if (x1 >= x2) swap (x1, x2); if (y1 >= y2) swap (y1, y2); if (z1 >= z2) swap (z1, z2); x.push_back ({max (0 , x1), min (a, x2)}); y.push_back ({max (0 , y1), min (b, y2)}); z.push_back ({max (0 , z1), min (c, z2)}); } cout << max ({getRes (x), getRes (y), getRes (z)}) << endl; }
# 写法 2 - 离散化差分利用 map
自动排序的特性,进行离散化差分
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void solve () { map<int , int > mp; cin >> n >> a >> b >> c; mp[a] = mp[b] = mp[c] = 0 ; vector<array<int , 6>> t (n + 1 ); for (int i = 1 ; i <= n; i++) { for (auto & tt : t[i]) cin >> tt; auto &[x1, y1, z1, x2, y2, z2] = t[i]; if (x1 >= x2) swap (x1, x2); if (y1 >= y2) swap (y1, y2); if (z1 >= z2) swap (z1, z2); mp[x1] = mp[x2] = mp[y1] = mp[y2] = mp[z1] = mp[z2] = 0 ; } int idx = 0 ; for (auto & [t1, t2] : mp) t2 = ++idx; vector<int > cfx (idx + 2 ) , cfy (idx + 2 ) , cfz (idx + 2 ) ; for (int i = 1 ; i <= n; i++) { auto &[x1, y1, z1, x2, y2, z2] = t[i]; cfx[mp[x1]]++, cfx[mp[x2] + 1 ]--; cfy[mp[y1]]++, cfy[mp[y2] + 1 ]--; cfz[mp[z1]]++, cfz[mp[z2] + 1 ]--; } int res = 0 ; for (int i = 1 ; i <= idx; i++) { cfx[i] += cfx[i - 1 ], cfy[i] += cfy[i - 1 ], cfz[i] += cfz[i - 1 ]; res = max ({res, cfx[i], cfy[i], cfz[i]}); } cout << res << endl; }
# 题目大意现在小 A A A 和小 B B B 在玩抛硬币游戏,一共有 n n n 枚最初都是正面朝上的硬币。现在他们有一个数字 k k k ,小 A A A 把 k k k 枚硬币翻转成反面朝上了,但是小 B B B 不知道有哪些硬币是被翻转了的,小 B B B 需要对每枚硬币执行下面四种操作中的一种
放入第一堆 翻转后放入第一堆 放入第二堆 翻转后放入第二堆 若把所有硬币放完后,两堆硬币中,正面朝上的数量相同,则小 B B B 赢游戏(允许某一堆硬币数为 0 0 0 ) 你现在要输出一个操作序列,使得对于小 A A A 的任意翻转顺序,小 B B B 都能赢
# 数据范围1 ≤ n ≤ 1 0 4 1 \le n \le 10^4 1 ≤ n ≤ 1 0 4 # 题解题目最终是要两堆操作后正面向上硬币相同,不妨列出以下表格
第一堆 第二堆 总数 k k k n − k n-k n − k 选取到的反面个数 x x x (假设)k − x k-x k − x 选取到的正面硬币个数 k − x k-x k − x n − 2 ∗ k + x n-2*k+x n − 2 ∗ k + x
我们可以发现,此时,第一堆的选取到的正面硬币个数 = 第二堆的选取到的反面硬币个数 ,所以我们只需要,k k k 个硬币选 1.
操作放入第一堆,n − k n-k n − k 个硬币选 4.
操作放入第二堆,即可实现题目要求
C++ 1 2 3 4 5 6 7 8 9 void solve () { int n, k; cin >> n >> k; for (int i = 1 ; i <= k; i++) cout << 1 ; for (int i = k; i < n; i++) cout << 4 ; cout << endl; }
# 题目大意对于小 A A A ,在接下来的 n n n 天中,若在第 i i i 天摄入了 r i r_i r i 卡路里,消耗了 c i c_i c i 克质量,那么他的能量损失是
c i = p × c i − 1 + ( 1 − p ) × r i − 1 c_i=p \times c_{i-1}+(1-p) \times r_{i-1} c i = p × c i − 1 + ( 1 − p ) × r i − 1
其中,r 0 , c 0 , p r_0,c_0,p r 0 , c 0 , p 已知 但是,小 A A A 在这 n n n 天中,有 k k k 天的放纵日,这 k k k 天的卡路里摄入量是预定的
除了放纵日的摄入,其余天的卡路里摄入量必须在 [ L , R ] [L,R] [ L , R ] 范围内 请你安排不是放纵日的日子,使得接下来 n n n 天的总能量损失最大,即 m a x ∑ i = 1 n ( c i − r i ) max \sum\limits_{i=1}^n(c_i-r_i) m a x i = 1 ∑ n ( c i − r i )
# 输入格式第一行包含两个整数 n n n 和 k k k ,表示未来有 n n n 天,k k k 天有预定摄入量 第二行包含五个实数 r 0 , c 0 , p , L , R r_0,c_0,p,L,R r 0 , c 0 , p , L , R 接下来 k k k 行,每行有一个整数 p i p_i p i 和一个实数 v i v_i v i ,表示 r p i r_{p_i} r p i 是 v i v_i v i
# 数据范围1 ≤ k ≤ n ≤ 1 0 5 1 \le k \le n \le 10^5 1 ≤ k ≤ n ≤ 1 0 5 1 ≤ n ≤ 2 × 1 0 5 1 \le n \le 2 \times 10^5 1 ≤ n ≤ 2 × 1 0 5 1 ≤ L ≤ r 0 ≤ R ≤ 1 0 9 1 \le L \le r_0 \le R \le 10^9 1 ≤ L ≤ r 0 ≤ R ≤ 1 0 9 0 < p < 1 0 < p < 1 0 < p < 1 1 ≤ c 0 ≤ 1 × 1 0 9 1 \le c_0 \le 1 \times 10^9 1 ≤ c 0 ≤ 1 × 1 0 9 # 题解根据题目给出的公式有
c i − r i = p × c i − 1 + ( 1 − p ) × r i − 1 − r i c_i-r_i=p \times c_{i-1}+(1-p) \times r_{i-1}-r_i c i − r i = p × c i − 1 + ( 1 − p ) × r i − 1 − r i
求和有
∑ i = 1 n ( c i − r i ) = ∑ i = 1 n ( p × c i − 1 + ( 1 − p ) × r i − 1 − r i ) \sum\limits_{i=1}^{n}{(c_i-r_i)}=\sum\limits_{i=1}^{n}{(p \times c_{i-1} + (1-p) \times r_{i-1} - r_i)} i = 1 ∑ n ( c i − r i ) = i = 1 ∑ n ( p × c i − 1 + ( 1 − p ) × r i − 1 − r i )
展开有
∑ i = 1 n ( c i − r i ) = p × ∑ i = 1 n ( c i − 1 − r i − 1 ) + ∑ i = 1 n r i − 1 − ∑ i = 1 n r i \sum\limits_{i=1}^{n}{(c_i-r_i)}=p \times \sum\limits_{i=1}^{n}{(c_{i-1}-r_{i-1})}+ \sum\limits_{i=1}^{n}{r_{i-1}}-\sum\limits_{i=1}^{n}{r_i} i = 1 ∑ n ( c i − r i ) = p × i = 1 ∑ n ( c i − 1 − r i − 1 ) + i = 1 ∑ n r i − 1 − i = 1 ∑ n r i
移项并合并同类项有
( 1 − p ) × ∑ i = 1 n ( c i − r i ) = [ p × c 0 + ( 1 − p ) × r 0 ] − [ p × c n − ( 1 − p ) × r n ] = c 1 − c n + 1 (1-p) \times \sum\limits_{i=1}^{n}{(c_i-r_i)}= [p\times c_0 + (1-p) \times r_0] - [p \times c_n-(1-p) \times r_n]=c_1 - c_{n+1} ( 1 − p ) × i = 1 ∑ n ( c i − r i ) = [ p × c 0 + ( 1 − p ) × r 0 ] − [ p × c n − ( 1 − p ) × r n ] = c 1 − c n + 1
即
∑ i = 1 n ( c i − r i ) = c 1 − c n + 1 1 − p \sum\limits_{i=1}^{n}{(c_i-r_i)} = \frac{c_1-c_{n+1}}{1-p} i = 1 ∑ n ( c i − r i ) = 1 − p c 1 − c n + 1
因为答案是求 m a x ∑ i = 1 n ( c i − r i ) max \sum\limits_{i=1}^n(c_i-r_i) m a x i = 1 ∑ n ( c i − r i ) ,所以我们只需要让 c n + 1 c_{n+1} c n + 1 最小即可,又因为
c i = p × c i − 1 + ( 1 − p ) × r i − 1 c_i=p \times c_{i-1} + (1-p) \times r_{i-1} c i = p × c i − 1 + ( 1 − p ) × r i − 1
故我们每次只当没有指定的时候,让 r i − 1 r_{i-1} r i − 1 取最小值 L L L 即可
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 double r0, c0, p, L, R, day, v;void solve () { int n, k; cin >> n >> k; cin >> r0 >> c0 >> p >> L >> R; map<int , double > mp; for (int i = 0 ; i < k; i++) { cin >> day >> v; mp[day] = v; } double c1 = p * c0 + (1 - p) * r0; double cn = c1; for (int i = 2 ; i <= n + 1 ; i++) { if (!mp.count (i - 1 )) cn = p * cn + (1 - p) * L; else cn = p * cn + (1 - p) * mp[i - 1 ]; } printf ("%.10lf\n" , (c1 - cn) / (1 - p)); }
# 题目大意你有 n n n 个箭头,箭头可以朝向 ↑ ↓ ← →
四个方向,你有两种操作
选中一个箭头,选中的箭头方向不变,其他箭头顺时针旋转 90 ° 90° 9 0 ° 所有箭头顺时针旋转 90 ° 90° 9 0 ° 给定各个箭头初始时的朝向,如果要使所有箭头朝向变为 ↑
,最少需要操作多少次?
# 数据范围1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1 ≤ n ≤ 1 0 6 # 题解初始方向第 i i i 个指向 a i a_i a i 操作 1.
相当于,当前这个选中的第 i i i 的箭头逆时针旋转,即 -1
操作,但进行一次操作 1 1 1 ,会给全局带来一个 +1
的偏移量,所以我们可以暴力计算四个方向 0 1 2 3
,先统一 n n n 个箭头的方向,记操作数为 b i b_i b i 此时,n n n 个箭头的绝对指向是 ( i + b i ) % 4 (i + b_i) \% 4 ( i + b i ) % 4 ,所以我们只需要进行 (4 - (i + b[i]) % 4) % 4
次 2.
操作,即可将所有箭头都转至 0 0 0 故,最终答案是 b[i] + (4 - (i + b[i]) % 4) % 4
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void solve () { cin >> n; vector<int > a (n) , b (4 ) ; for (auto & t : a) cin >> t; for (int i = 0 ; i < n; i++) for (int j = 0 ; j < 4 ; j++) b[j] += (a[i] - j + 4 ) % 4 ; int res = 1e18 ; for (int i = 0 ; i < 4 ; i++) res = min (res, b[i] + (4 - (b[i] + i) % 4 ) % 4 ); cout << res << endl; }
写题目的时候,要注重相对 和整体系统 的概念,比如此题中的操作 1 1 1 ,它会使得选中的那个箭头,相对全局 − 1 -1 − 1 ,但是不要忽略了,他对系统整体方向 + 1 +1 + 1 的影响,最终考虑方向的时候要把这个 + 1 +1 + 1 考虑进去
以上五题是赛时开的题目,下面是赛后补的题
# 题目大意有一个 n n n 个点 m m m 条单向边的图 一开始,你有体力 x x x ,当你通过一条长为 d d d 的边时,你的体力会变成 ⌊ x d ⌋ \lfloor{\frac{x}{d}}\rfloor ⌊ d x ⌋ ,当你的体力变为 0 0 0 的时候,代表体力耗尽,你将不再继续探索 现在有 Q Q Q 个问题,对于第 i i i 个问题,请问,若从 p i p_i p i 点开始,初始体力为 x x x ,若想让体力耗尽,则最少要走多少条边?
# 数据范围1 ≤ n , Q ≤ 2 × 1 0 5 1 \le n, Q \le 2 \times 10^5 1 ≤ n , Q ≤ 2 × 1 0 5 n ≤ m ≤ 5 × 1 0 5 n \le m \le 5 \times 10^5 n ≤ m ≤ 5 × 1 0 5 1 ≤ u , v ≤ n , 2 ≤ d ≤ 1 0 9 1 \le u, v \le n, 2 \le d \le 10^9 1 ≤ u , v ≤ n , 2 ≤ d ≤ 1 0 9 1 ≤ x ≤ 1 0 9 1 \le x \le 10^9 1 ≤ x ≤ 1 0 9 # 题解这个题目的核心是,把走若干条边的除法转换为了 d 1 × d 2 × . . . × d . . . > x d_1 \times d_2 \times ... \times d_{...} > x d 1 × d 2 × . . . × d . . . > x
定义 D P DP D P 为 dp[i][j]
表示从 j j j 点出发,经过 i i i 步后,消耗乘积的最大值
d p [ i ] [ u ] = m a x { d u → v 1 × . . . × d v i − 1 → v i } dp[i][u] = max\{d_{u \to v_1} \times ... \times d_{v_{i-1} \to v_{i}}\} d p [ i ] [ u ] = m a x { d u → v 1 × . . . × d v i − 1 → v i }
那么状态转移方程为
d p [ i ] [ u ] = max ( u → v ) ∈ E ( d p [ i − 1 ] [ v ] × d u → v ) dp[i][u] = \max_{(u \to v) \in E} \left( dp[i - 1][v] \times d_{u \to v} \right) d p [ i ] [ u ] = ( u → v ) ∈ E max ( d p [ i − 1 ] [ v ] × d u → v )
x x x 最大值是 1 0 9 10^9 1 0 9 ,在 30 30 3 0 次内便可以除完
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int n, m, q, p, x;vector<PII> g[N]; void solve () { cin >> n >> m >> q; for (int i = 1 , u, v, w; i <= m; i++) cin >> u >> v >> w, g[u].push_back ({v, w}); vector<vector<int >> dp (31 , vector <int >(n + 1 , 1 )); for (int i = 1 ; i <= 30 ; i++) { vector<int > tmp (n + 1 , 1 ) ; for (int u = 1 ; u <= n; u++) { for (auto &[v, w] : g[u]) tmp[u] = max (tmp[u], dp[i - 1 ][v] * w); } dp[i] = tmp; } while (q--) { cin >> p >> x; for (int i = 1 ; i <= 30 ; i++) if (dp[i][p] > x) { cout << i << endl; break ; } } }
对于这种超大数据范围的图论题 、最短路 / D F S DFS D F S / B F S BFS B F S 走不通的题目 其实基本上不用考虑图论里的算法了,可以考虑下 D P DP D P ,特别是这种走若干步的,基本上不太可能 D F S DFS D F S / B F S BFS B F S 了
如果题目出现连除下取整,考虑转换为乘法 + D P DP D P 去做
# 题目大意我一天有 n n n 个时间段,一个字符串 s s s 描述我的日程安排
s i = 1 s_i = 1 s i = 1 表示我在 i i i 时间段很忙s i = 0 s_i = 0 s i = 0 表示我在 i i i 时间段有空现在我必须 执行一次 这个操作:
选择一个 范围 [ l , r ] [l, r] [ l , r ] ,l → r l \to r l → r 有 k k k 个时间段很忙(k k k 是一个确定的常数) 选择后,任意修改 l → r l \to r l → r 的忙 / 闲状态,但是你需要保证的是,修改后 l → r l \to r l → r 中,忙的时间段的数量和修改前是一样的 请问,我最多可以得到多少个不同的时间表?答案很大,需模 998244353 998244353 9 9 8 2 4 4 3 5 3
# 数据范围1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1 ≤ n ≤ 1 0 5 # 题解需要预处理的数据
fac[i]
:表示 i i i 的阶层inv[i]
:表示 i i i 的阶层的逆元suf[i]
:表示 i i i 位置后连续的 0 0 0 的长度因为操作只能做一次,我们可以用双指针滑动窗口找满足 1 1 1 的个数恰好是 k k k 的区间 对于一个合法的操作窗口,我们的答案累加上
C ( r − l + 1 ) + suf i k C_{(r - l + 1) + \text{suf}_i}^{k} C ( r − l + 1 ) + suf i k
代表从 窗口
+ 窗口后连续的 1
进行排列组合,选取 k k k 个位置放 1 1 1 用一个 f l a g flag f l a g 标记是否是第一次操作滑动窗口,如果不是第一次操作滑动窗口,则还需要去重,需要减去
C r − l k − 1 C_{r-l}^{k-1} C r − l k − 1
为什么要减?因为 新窗口 = 旧窗口 + 下面新延伸到的第一个 1
而对于保持新延伸到的 1 1 1 不变 的方案,是之前重复了的 所以需要减去,最后一个 1 1 1 确定放在最后一个位置,前面的 k − 1 k - 1 k − 1 个 1 1 1 在 r − l r-l r − l 区间中随机放的方案数
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 const int N = 1e5 + 5 , MOD = 998244353 ;vector<int > fac (N) , inv (N) ;int qmi (int a, int b) { int res = 1 ; while (b) { if (b & 1 ) res = res * a % MOD; a = a * a % MOD; b >>= 1 ; } return res; } int cal (int n, int k) { return fac[n] * inv[k] % MOD * inv[n - k] % MOD; } void solve () { int n, k; string s; cin >> n >> k >> s; s = " " + s; vector<int > suf (n + 2 , 0 ) ; int cur = 0 ; for (int i = n; i >= 1 ; i--) { cur += (s[i] == '0' ); suf[i] = cur; if (s[i] == '1' ) cur = 0 ; } int ans = 0 , cnt = 0 ; bool flag = false ; for (int l = 1 , r = 1 ; r <= n; r++) { if (s[r] == '1' ) { cnt++; while (l <= r && cnt > k) { if (s[l] == '1' ) cnt--; l++; } if (cnt == k) { ans = (ans + cal ((r - l + 1 ) + suf[r], k)) % MOD; if (!flag) flag = true ; else ans = (ans - cal (r - l, k - 1 ) + MOD) % MOD; } } } if (k == 0 ) ans = 1 ; cout << ans << endl; } signed main () { ios::sync_with_stdio (false ); cin.tie (0 ), cout.tie (0 ); fac[0 ] = 1 ; for (int i = 1 ; i < N; i++) fac[i] = fac[i - 1 ] * i % MOD; inv[N - 1 ] = qmi (fac[N - 1 ], MOD - 2 ); for (int i = N - 2 ; i >= 0 ; i--) inv[i] = inv[i + 1 ] * (i + 1 ) % MOD; int _ = 1 ; cin >> _; while (_--) solve (); return 0 ; }
# 题目大意定义一个k k k 折叠字符串 的概念
当且仅当 存在一个字符串 t t t ,使得 s = t k s = t^k s = t k ,即 t t t 重复连续 k k k 次恰好形成 s s s 对于给定的字符串 S S S ,每个查询给出正整数 x x x 和 y y y ,请问有多少对正整数 l , r l, r l , r 满足
x ≤ l < r ≤ y x \le l < r \le y x ≤ l < r ≤ y S [ l , R ] S_{[l, R]} S [ l , R ] 可以重排成 k k k 折叠字符串你需要回答 q q q 个查询
# 数据范围1 ≤ n , k , q ≤ 3 × 1 0 5 1 \le n, k, q \le 3 \times 10^5 1 ≤ n , k , q ≤ 3 × 1 0 5 时间限制 3 s e c o n d s 3~seconds 3 s e c o n d s # 题解重排字符串很容易能理解成,对于 S [ l , r ] S_{[l, r]} S [ l , r ] ,其内部所有字符的出现次数,都能被 k k k 整除
我们统计一个 vector<int> cnt(26)
计算各个位置上的字符个数(均为对 k k k 取模后的值) 那么,如果遍历到 i i i 时候计算得到的 c n t i cnt_i c n t i 和遍历到 j j j 得到的 c n t j cnt_j c n t j 数组完全相同的话(i < j i < j i < j ),那么即说明,S [ i + 1 , j ] S_{[i+1, j]} S [ i + 1 , j ] 是 k k k 折叠字符串可以把第 i i i 个位置的 vector<int> cnt(26)
哈希至 hash[i]
所以原问题被转换为,对于每个查询 ( x , y ) (x,y) ( x , y ) ,能找到多少对 ( i , j ) (i, j) ( i , j ) 满足
x − 1 ≤ i < j ≤ y x - 1 \le i < j \le y x − 1 ≤ i < j ≤ y hash[i] == hash[j]
为了方便描述解释,此处先给出代码
cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 int n, k, q, idx, res;string s; vector<int > pos (N) ;vector<int > c; struct Md { int l, r, id; }; void add (int x) { res += c[x]; c[x]++; } void sub (int x) { c[x]--; res -= c[x]; } void solve () { cin >> n >> k >> q; cin >> s, s = " " + s; int len = sqrt (n); vector<int > cnt (26 , 0 ) ; map<vector<int >, int > mp; vector<int > hash (n + 1 ) ; mp[cnt] = 0 ; for (int i = 1 ; i <= n; i++) { cnt[s[i] - 'a' ]++, cnt[s[i] - 'a' ] %= k; if (!mp.count (cnt)) mp[cnt] = ++idx; hash[i] = mp[cnt], pos[i] = i / len; } c = vector <int >(idx + 1 , 0 ); vector<Md> md (q + 1 ) ; for (int i = 1 ; i <= q; i++) cin >> md[i].l >> md[i].r, md[i].id = i; sort (md.begin () + 1 , md.begin () + q + 1 , [](const Md &a, const Md &b) { return pos[a.l] == pos[b.l] ? a.r < b.r : pos[a.l] < pos[b.l]; }); vector<int > ans (q + 1 ) ; int cl = 0 , cr = 0 ; add (hash[0 ]); for (int i = 1 ; i <= q; i++) { auto &que = md[i]; int l = que.l - 1 , r = que.r, id = que.id; cout << "l: " << l << ", r: " << r << ", id: " << id << endl; while (cl > l) add (hash[--cl]); while (cr < r) add (hash[++cr]); while (cl < l) sub (hash[cl++]); while (cr > r) sub (hash[cr--]); ans[id] = res; } for (int i = 1 ; i <= q; i++) cout << ans[i] << endl; }
如何理解两个关键函数 add
和 sub
?C++ 1 2 3 4 5 6 7 8 9 void add (int x) { res += c[x]; c[x]++; } void sub (int x) { c[x]--; res -= c[x]; }
分别以右边加一个值 和左边减一个值 为例,进行说明 我们的目的是找对于给定的 [ x , y ] [x, y] [ x , y ] 中,能找到多少个 ( i , j ) (i,j) ( i , j ) 满足 x − 1 ≤ i < j ≤ y x-1 \le i < j \le y x − 1 ≤ i < j ≤ y 并且 hash[i] == hash[j]
直接暴力计算是肯定不行的,我们可以考虑利用桶 来计数,用 c [ h a s h [ t ] ] c[hash[t]] c [ h a s h [ t ] ] 计算已有的 h a s h [ t ] hash[t] h a s h [ t ] 出现次数,那么当新出现一个 h a s h [ t ] hash[t] h a s h [ t ] 的时候,它可以和前面已经有的 h a s h [ t ] hash[t] h a s h [ t ] 组成 k k k 折叠字符串,也就是可以加上 c [ h a s h [ t ] ] c[hash[t]] c [ h a s h [ t ] ] 而在莫队的过程中当右边加上一个值时(假计下标为 t t t ),也就是说,加上 t t t 位置后,多了一个 hash[t]
,这个新的 hash[t]
可以和前面已经有的 hash[t]
组成 k k k 折叠序列,所以应该先让 r e s res r e s 加上已有的 c[hash[t]]
再让 c[hash[t]]++
当左边减去一个值时(假计下标为 t t t ),也就是说,减去 t t t 位置后,少了一个 hash[t]
,而借鉴前面的加上一个值的过程,实际上 k k k 折叠字符串其实是少了 c[hash[t]] - 1
个,所以我们先让 c[hash[t]]--
,再让答案 r e s res r e s 减去 c[hash[t]]
为什么要先 add(hash[0])
? 因为我们查询的是在区间 [x - 1, y]
中,这样的 (i, j)
对有多少个满足 i < j
并且 hash[i] == hash[j]
,那么一开始 hash[0]
也要被加入进去,因为 cnt{0, 0, ..., 0}
对于后面 26 26 2 6 个字母全部都出现 i k ik i k 次,实际上是一个前缀,也就是说 hash[0]
要被第一个加入,即使可能很少被运算到