题意:已知递增的数组 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1]和 b [ 0 : n − 1 ] b[0:n-1] b[0:n−1],其中 b [ 0 : n − 1 ] b[0:n-1] b[0:n−1]是由 a [ 0 : n − 1 ] + d [ 0 : n − 1 ] a[0:n-1]+d[0:n-1] a[0:n−1]+d[0:n−1]并重新排序之后得到的,其中 d [ 0 : n − 1 ] ≥ 0 d[0:n-1]\ge0 d[0:n−1]≥0。现在希望求出每个 d [ i ] d[i] d[i]可能取值的最大值和最小值。
int j = n - 1, aviMax = n - 1;
for (int i = n - 1; i >= 0; i--) {
while (j - 1 >= 0 && b[j - 1] >= a[i]) j--;
ansMax[i] = b[aviMax], ansMin[i] = b[j];
if (j == i) aviMax = j - 1;
}
提示: 假设上次访问过的状态之后不再需要,可以考虑双指针法。
题意:给定两个数列 a [ 0 : n − 1 ] , b [ 0 : n − 1 ] a[0:n-1],b[0:n-1] a[0:n−1],b[0:n−1],重排后最大化:
And i = 0 n − 1 ( a i Xor b i ) \text{And}~_{i=0}^{n-1}(a_i~\text{Xor}~b_i) And i=0n−1(ai Xor bi)
bool judge(long long ans)
{
map<long long, int> cnt;
for (int i = 0; i < n; i++) cnt[a[i] & ans] ++, cnt[~b[i] & ans] --;
bool ret = true;
for (auto it = cnt.begin(); it != cnt.end(); it++) if (it->second != 0) {
ret = false;
break;
}
return ret;
}
提示: 一个问题可以分很多步解决,第一步往往有重要的启发价值。那么,究竟是把第一步作为基本操作、用程序逻辑保证其正确性,还是把第一步作为某个更通用操作的特殊化呢?
题意:给定字符串 s , t 1 , ⋯ , t q s,t_1,\cdots,t_q s,t1,⋯,tq, ∣ t ∣ |t| ∣t∣都比较小并且字符集为 { a , ⋯ , z } \{a,\cdots,z\} {a,⋯,z}。现在要对所有的 i = 1 , ⋯ , q i=1,\cdots,q i=1,⋯,q查询 s + t i s+t_i s+ti的前缀函数 π \pi π在 ∣ s ∣ |s| ∣s∣到 ∣ s ∣ + ∣ t i ∣ − 1 |s|+|t_i|-1 ∣s∣+∣ti∣−1的取值。
vector<int> pi(n + Maxt);
for (int i = 1; i < n; i++) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) j++;
pi[i] = j;
}
while (q--) {
string t;
cin >> t;
int m = t.length();
s += t;
for (int i = n; i < n + m; i++) {
for (int c = 0; c < Al; c++) {
if ('a' + c != s[i]) aut[i][c] = aut[pi[i - 1]][c];
else aut[i][c] = i + ('a' + c == s[i]);
}
pi[i] = aut[pi[i - 1]][s[i] - 'a'];
}
for (int i = n; i < n + m; i++) printf("%d ", pi[i]);
s.erase(s.begin() + n, s.end());
printf("\n");
}
提示:
(1) 前缀函数的传统计算方法拥有均摊线性的复杂度,而构建前缀函数自动机的线性复杂度并非均摊的,可以认为其时间复杂度直接正比于字符串长度;
(2) 前缀函数的传统计算方法表明,在具有相同前缀函数值的位置后加上相同的字符,该字符位置的前缀函数值都是相同的;
(2) 前缀函数自动机的构建过程中,假设要针对状态 0 , ⋯ , n − 1 0,\cdots,n-1 0,⋯,n−1构建自动机,就需要拥有 π ( 0 : n − 1 ) \pi(0:n-1) π(0:n−1)的信息,具体来说在构建状态 i i i的转移表时必须已知 π ( 0 : i − 1 ) \pi(0:i-1) π(0:i−1)并且构建完毕后需要更新 π ( i ) \pi(i) π(i)。
题意:一个序列划分成连续相等子序列时,子序列数目的最小值称为这个序列的 A A A值,要求维护一个定长序列,支持单点修改和查询所有非零子序列 A A A值之和。
while (m--) {
int idx, x;
scanf("%d %d", &idx, &x);
if (idx > 1 && ((a[idx] == a[idx - 1]) ^ (x == a[idx - 1]))) ans += ((x == a[idx - 1]) ? (-1LL) : 1LL) * (idx - 1) * (n - idx + 1);
if (idx < n && ((a[idx] == a[idx + 1]) ^ (x == a[idx + 1]))) ans += ((x == a[idx + 1]) ? (-1LL) : 1LL) * idx * (n - idx);
a[idx] = x;
printf("%lld\n", ans);
}
提示:
(1) A A A值实际上就是包含分割点的数目 + 1 +1 +1,而一个分割点之间是相互独立的,这是因为每引入(移除)一个分割点, A A A值受到影响的子序列数目是固定的并且改变的值恰好就是子序列数目;
(2) 可以利用上界估计考察数据范围。
题意:已知有一数组 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1]满足 q q q个约束条件,第 k k k个约束条件要求 a i k Or a j k = x k a_{i_k}~\text{Or}~a_{j_k}=x_k aik Or ajk=xk,求满足这些约束条件的字典序最小的 a a a。
for (int i = 1; i <= n; i++) if (!fix[i]) {
if (g[i].size() > 0) {
int ans = 0;
for (Node x : g[i]) ans |= (a[x.idx] ^ x.req);
a[i] &= ans;
}
else a[i] = 0;
}
提示: 在一个决策阶段中为了满足约束条件需要考虑哪些因素?
题意:给定非负权值无向图 G = ( V , E ) G=(V,E) G=(V,E),求从节点 1 1 1到其他所有节点的最短距离。一条从 1 1 1到 i i i的路径是一个图中的节点序列 1 = v 0 → ⋯ → v d = i 1=v_0\to\cdots\to v_d=i 1=v0→⋯→vd=i,其距离计算为 d ( v 0 , v 1 ) + ⋯ d ( v d − 1 , v d ) 。 d(v_0,v_1)+\cdots d(v_{d-1},v_d)。 d(v0,v1)+⋯d(vd−1,vd)。其中每对 d ( v j − 1 , v j ) d(v_{j-1},v_j) d(vj−1,vj)的计算方式可以从下面两种方式中选择:
( 1 ) ( v j − 1 , v j ) ∈ E (1) (v_{j-1},v_j)\in E (1)(vj−1,vj)∈E时,可以令 d ( v j − 1 , v j ) = w ( v j − 1 , w j ) d(v_{j-1},v_j)=w(v_{j-1},w_j) d(vj−1,vj)=w(vj−1,wj)
( 2 ) d ( v j − 1 , v j ) = ( v j − 1 − v j ) 2 (2) d(v_{j-1},v_j)=(v_{j-1}-v_{j})^2 (2)d(vj−1,vj)=(vj−1−vj)2
并且一条路径中采取 ( 2 ) (2) (2)计算的相邻节点对不得超过事先给定的正整数 k k k。
priority_queue<pair<long long, int>> q;
long long dis[Maxn];
bool vis[Maxn];
void dijkstra()
{
for (int i = 1; i <= n; i++) q.push({ -dis[i], i }), vis[i] = false;
while (!q.empty()) {
int cur = q.top().second; q.pop();
if (!vis[cur]) {
vis[cur] = true;
for (auto nxt : g[cur]) {
if (dis[nxt.first] > dis[cur] + nxt.second) {
dis[nxt.first] = dis[cur] + nxt.second;
q.push({ -dis[nxt.first], nxt.first });
}
}
}
}
}
while (k--) {
vector<Line> lines;
vector<double> p;
for (int i = 1; i <= n; i++) {
Line cur = { -i * 2LL, dis[i] + 1LL * i * i };
while (lines.size() >= 2 && cur.ins(lines[lines.size() - 2]) <= p.back()) {
lines.pop_back();
p.pop_back();
}
if (!lines.empty()) p.push_back(cur.ins(lines.back()));
lines.push_back(cur);
}
for (int i = 1; i <= n; i++) {
int j = upper_bound(p.begin(), p.end(), i) - p.begin();
dis[i] = min(dis[i], lines[j].k * i + lines[j].b + 1LL * i * i);
}
dijkstra();
}
提示:
(1) Dijkstra \text{Dijkstra} Dijkstra算法求单源最短路,每次从待扩展节点中选择距离最小的节点并对其出边松弛,这一过程的正确性依赖于 ( 1 ) (1) (1)非负边权 ( 2 ) (2) (2)当某个节点被扩展时它被维护的距离值恰好是从起点到该节点的最短路径长度,虽然路径的概念因题而异,但 Dijkstra \text{Dijkstra} Dijkstra算法并不要求所求出的最短距离必须是经图中边可达的;
(2) Convex Hull Trick \text{Convex Hull Trick} Convex Hull Trick或斜率优化、凸包优化可以用于优化形如
dp ( i ) = min j < i { dp ( j ) + Cost ( i , j ) } \text{dp}(i)=\min_{jdp(i)=j<imin{dp(j)+Cost(i,j)}
的动态规划问题(对 Cost \text{Cost} Cost函数有一点要求),时间复杂度从 O ( n 2 ) \mathcal{O}(n^2) O(n2)优化到 O ( n log n ) \mathcal{O}(n\log n) O(nlogn);
(3) 动态规划问题中,往往需要限制按照某种方法转移的次数,此时需要注意当前层转移的结果有没有被当前层后来的转移过程利用,滚动数组的优化方法往往出自于此;
(4) 动态规划问题中,假设某种特殊的转移方法可能出现在决策的任意一步中,可以先考虑以其为最后一步的全体决策,再在决策之后的基础上继续转移。
题意:给定一棵 n n n层的完全二叉树,叶子节点编号 1 1 1到 2 n 2^n 2n。每一个内点到两个子节点的边的其中一条被标记了,此时只有唯一一个叶子节点到根节点有一条每条边都被标记的路径,我们称其为目标节点。现在要求你为每个内点选择其要标记的边(左子节点或右子节点关联的边),并把叶子节点重新排列,之后有不超过 k k k个内点的选择会被更改,试最小化在所有可能的更改下目标节点编号的最大值并求这个最大值(对 1 e 9 + 7 1e9+7 1e9+7取模的结果)。
long long ans = 0;
for (int i = 0; i <= k; i++)
ans = (ans + (((fact[n] * inv(fact[i])) % Mod) * inv(fact[n - i])) % Mod) % Mod;
提示:
(1) 如果要考虑 max { a [ i d x 1 ] , ⋯ , a [ i d x m ] } \max\{a[idx_1],\cdots,a[idx_m]\} max{a[idx1],⋯,a[idxm]},这里 m m m和 i d x i idx_i idxi都由给定的条件决定,而 a [ 0 : len − 1 ] a[0:\text{len}-1] a[0:len−1]的一个排列是已知的,并且 i ↦ a [ i ] i\mapsto a[i] i↦a[i]的对应关系可以任意更改,那么只需要考虑条件如何决定 m m m就行了。也即,由于我们的自由合法操作,一些被动操作是无效的;
(2) 如果我们想知道合法操作下目标值的种数,可以在等效的操作之间建立等价关系,从而我们只需要考虑每个等价类的代表元就行了;
(3) 组合数并非一定具有较高的复杂度,如果所涉及的组合数都有这样的形式:
( n m ) , n , m ≤ M \binom{n}{m},n,m\le M (mn),n,m≤M
那么通过 O ( M ) \mathcal{O}(M) O(M)对阶乘预处理就可以通过算术方法计算组合数。
(4) 模意义下计算乘法需要引入乘法逆元。
题意:给定正整数 n ( ≥ 3 ) n(\ge3) n(≥3),求
∑ a , b , c ≥ 1 , a + b + c = n lcm ( c , gcd ( a , b ) ) \sum_{a,b,c\ge1,a+b+c=n}\text{lcm}(c,\gcd(a,b)) a,b,c≥1,a+b+c=n∑lcm(c,gcd(a,b))
void pre(int n)
{
pri.reserve(n);
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!notPri[i]) {
pri.push_back(i);
phi[i] = i - 1;
}
for (int j : pri) {
if (1LL * i * j > n) break;
notPri[i * j] = true;
phi[i * j] = (j - (i % j != 0)) * phi[i];
if (i % j == 0) break;
}
}
}
for (int d = 1; d <= n - 1; d++)
for (int s = 2 * d; s < n; s += d) {
int c = n - s;
ans = (ans + (1LL * lcm(c, d) * phi[s / d]) % Mod) % Mod;
}
提示:
(1) 与 gcd \gcd gcd有关的问题往往可以约去 gcd \gcd gcd,这时问题的结构会更清晰;
(2) Eratosthenes \text{Eratosthenes} Eratosthenes筛法可以改良为每次让一个合数被它的最小质因子筛去,可以保证 O ( n ) \mathcal{O}(n) O(n)的时间复杂度。
题意:给定 n × m n\times m n×m的 01 01 01矩阵,要求每次用 0 0 0覆盖 L L L型的相邻三个位置(前提是这三个位置不全为 0 0 0),求最多可进行多少次这样的操作。
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < m - 1; j++) {
int occup = a[i][j] - '0' + a[i + 1][j] - '0' + a[i][j + 1] - '0' + a[i + 1][j + 1] - '0';
spare = max(spare, 4 - occup);
if (spare >= 2) goto out;
}
}
out:
提示: 寻找上界再考虑取等。
题意: 给定数组 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1],求出最长的子序列长度,使之这个子序列满足对任意的相邻两项 a [ i ] , a [ j ] ( i < j ) a[i],a[j](i
for (int i = 1; i < n; i++)
for (int j = max(0, i - Len + 1); j < i; j++)
if ((a[j] ^ i) < (a[i] ^ j)) {
dp[i] = max(dp[i], dp[j] + 1);
ans = max(dp[i], ans);
}
提示:
(1) 求最长上升(不降)子序列的朴素动态规划解法保证得到的子序列相邻两项满足“序”关系,这里这个序关系不一定满足偏序关系的要求;而 O ( n log n ) \mathcal{O}(n\log n) O(nlogn)的优化解法为了使最终的子序列相邻两项满足序关系,必须有更多的要求;
(2) 在 b b b进制中,若 x − y > 2 b x-y>2^b x−y>2b,那么在第 b b b位(从 0 0 0开始计)及以上一定有 x ′ > y ′ x'>y' x′>y′,这里 ( ⋅ ) ′ (\cdot)' (⋅)′表示一个数除去低 0 ∼ b − 1 0\sim b-1 0∼b−1位剩下的部分。
题意:去掉了 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1]有一个较小的上界这一条件。
for (int i = 0; i < n; i++) {
int cur = 0;
int key = a[i] ^ i;
int curAns = 1;
for (int j = Maxb; j >= 0; j--) {
if (idx[cur][BIT(~key, j)])
curAns = max(curAns, dp[(idx[cur][BIT(~key, j)])][BIT(~a[i], j)] + 1);
if (!idx[cur][BIT(key, j)]) break;
cur = idx[cur][BIT(key, j)];
}
ans = max(ans, curAns);
cur = 0;
for (int j = Maxb; j >= 0; j--) {
if (!idx[cur][BIT(key, j)]) idx[cur][BIT(key, j)] = create();
dp[idx[cur][BIT(key, j)]][BIT(i, j)] = max(dp[idx[cur][BIT(key, j)]][BIT(i, j)], curAns);
cur = idx[cur][BIT(key, j)];
}
}
提示:
(1) 给两个等长数字串比较字典序,只需要比较第一个不同位置上的数字大小关系;
(2) x Xor y = x ′ Xor y ′ x~\text{Xor}~y=x'~\text{Xor}~y' x Xor y=x′ Xor y′当且仅当 x Xor y ′ = x ′ Xor y x~\text{Xor}~y'=x'~\text{Xor}~y x Xor y′=x′ Xor y;
(3) bit trie \text{bit trie} bit trie即 01 01 01字典树往往可用于维护关于异或的信息。
题意:给定两个数组 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1]和 b [ 0 : n − 1 ] b[0:n-1] b[0:n−1],每次可以把其中一个数组中的其中一个元素变成它的位数,考虑最少的操作次数使得 a , b a,b a,b在重新排列之后可以相等。
// my code
int ans = 0;
for (auto i = rec.begin(); i != rec.end(); i++) if (i->first >= 10 && i->second != 0) {
int num = F(i->first);
if (rec.find(num) == rec.end()) rec[num] = i->second;
else rec[num] += i->second;
ans += abs(i->second);
}
for (auto i = rec.begin(); i != rec.end(); i++)
if (i->first < 10 && i->first > 1 && i->second != 0)
ans += abs(i->second);
// Maybe a better one
while (!qa.empty()){
if (qa.top() == qb.top()) qa.pop(), qb.pop();
else {
ans ++;
if (qa.top() > qb.top()){
qa.push(to_string(qa.top()).size());
qa.pop();
}
else{
qb.push(to_string(qb.top()).size());
qb.pop();
}
}
}
提示:
(1) 可以分步解决一个问题,从而只考虑步与步之间的归纳、递推关系以及每一步要做什么;也可以从全局考虑,设计出一个完整的、一致的方案;
(2) 从最具特殊性的元素开始考虑,如果每一个元素只需要被考虑一次就可以重复寻找最具特殊性的元素。
题意: A , B A,B A,B两人轮流从给定的偶数长度字符串首尾挑选字母,不断接在已经组成的字符串的首端,最终字符串字典序更小的一方获胜。如果两个人都采取最优策略,最终的结果是谁赢?
int dp[Maxn][Maxn]; // dp[i][j] means the result from s[i : j - 1]
int cmp(int al, int bo)
{
if (al == bo) return 0;
else return (al > bo) ? 1 : -1;
}
int res(int i, int j, char al, char bo) { return (dp[i][j] != 0) ? (dp[i][j]) : (cmp(al, bo)); }
for (int l = 2; l <= n; l += 2) {
for (int i = 0; i + l <= n; i++) {
int j = i + l;
// calculate dp[i][j] from dp[i'][j]' for all i <= i' <= j' <= j
dp[i][j] =
max(min(res(i + 2, j, s[i], s[i + 1]), res(i + 1, j - 1, s[i], s[j - 1])),
min(res(i + 1, j - 1, s[j - 1], s[i]), res(i, j - 2, s[j - 1], s[j - 2])));
}
}
提示:
(1) 动态规划可以有效利用已经计算出的结果来导出需要计算的内容;另外,在区间上进行常规意义下的区间动态规划时间复杂度为 O ( n 2 ) \mathcal{O}(n^2) O(n2),带断点的区间动态规划时间复杂度为 O ( n 3 ) \mathcal{O}(n^3) O(n3);
What do we do, when the array loses elements only from the left or from the right and the constraints obviously imply some quadratic solution? Well, apply dynamic programming, of course. \text{What do we do, when the array loses elements only from the left or}\\ \text{from the right and the constraints obviously imply some quadratic}\\ \text{solution? Well, apply dynamic programming, of course.} What do we do, when the array loses elements only from the left orfrom the right and the constraints obviously imply some quadraticsolution? Well, apply dynamic programming, of course.
(2) 不应陷入到分析策略优化的近似算法中,而是要通过最优解的性质/递推关系得出最优解。
题意:给定数组 a [ 0 : n − 1 ] a[0:n-1] a[0:n−1]和 b [ 0 : n − 1 ] b[0:n-1] b[0:n−1],给定 m m m个询问,对第 j j j个询问需要考虑所有满足 a j x + b j y = n , 0 ≤ a j x , b j y ≤ n a_jx+b_jy=n,0\le a_jx,b_jy\le n ajx+bjy=n,0≤ajx,bjy≤n的 ( x , y ) (x,y) (x,y),然后从 a a a中选择 a j x a_jx ajx个元素和 b b b中选择 b j y b_jy bjy个元素并求和,考虑和的最大值。
long long exgcd(long long a, long long b, long long c, long long& x, long long& y)
{
if (b == 0) {
x = c / a, y = 0;
return a;
}
long long ret = exgcd(b, a % b, c, x, y);
long long tx = y, ty = x - (a / b) * y;
x = tx, y = ty;
return ret;
}
while (m--) {
long long x, y;
scanf("%lld %lld", &x, &y);
long long sx, sy;
long long d = exgcd(x, y, n, sx, sy);
if (n % d != 0) printf("-1\n");
else {
long long ix = x / d, iy = y / d;
long long base = (1. * idx / y - sy) / ix;
long long ans = -1;
for (int i = -2; i <= 2; i++) {
long long cur = y * (sy + (base + i) * ix);
if (cur >= 0 && cur <= n) ans = max(ans, sA + sD[cur]);
}
printf("%lld\n", ans);
}
}
提示:
(1) 扩展 Euclid \text{Euclid} Euclid算法 可以用于求整系数线性方程 a x + b y = c ax+by=c ax+by=c的一组整数解,假设 gcd ( a , b ) = d \gcd(a,b)=d gcd(a,b)=d, ( x ∗ , y ∗ ) (x^*,y^*) (x∗,y∗)是一组特解,那么通解可以写成 ( x ∗ + k ⋅ b d , y ∗ − k ⋅ a d ) (x^*+k\cdot\frac{b}{d},~y^*-k\cdot\frac{a}{d}) (x∗+k⋅db, y∗−k⋅da),其中 k k k是任意的整数;
(2) 假设目标量依赖于某个决策,例如在 { 1 , ⋯ , n } \{1,\cdots,n\} {1,⋯,n}中选择一个数 k k k,那么可以把目标量看成与之有关的数列 a k a_k ak,为了快速计算其最优的位置可以考虑通过恰当的排序让其具有良好的单调性( Δ a k \Delta a_k Δak具有一致的符号)或者凹凸性( Δ 2 a k \Delta^2 a_k Δ2ak具有一致的符号),这样就只需要在有可能取到最值的位置附近去考虑。
题意:给定正整数 k k k,将 1 , ⋯ , n 1,\cdots,n 1,⋯,n( n n n是偶数)分成 n / 2 n/2 n/2对数,使得每一对 ( x j , y j ) (x_j,y_j) (xj,yj)满足 ( x j + k ) ⋅ y j ≡ 0 ( mod 4 ) (x_j+k)\cdot y_j\equiv0~(\text{mod}~4) (xj+k)⋅yj≡0 (mod 4),给出一种可行的分法(或者判断它不存在)。
// my code
if (frt.size() + bac.size() < rab.size()) printf("NO\n");
else {
vector<pair<int, int> > ans;
printf("YES\n");
while (!frt.empty()) {
if (!rab.empty()) ans.push_back({ frt.front(), rab.front() }), frt.pop(), rab.pop();
else if (!cor.empty()) ans.push_back({ frt.front(), cor.front() }), frt.pop(), cor.pop();
else if (!bac.empty()) ans.push_back({ frt.front(), bac.front() }), frt.pop(), bac.pop();
}
while (!bac.empty()) {
if (!rab.empty()) ans.push_back({ rab.front(), bac.front() }), bac.pop(), rab.pop();
else if (!cor.empty()) ans.push_back({ cor.front(), bac.front() }), bac.pop(), cor.pop();
}
while (!cor.empty()) {
int left = cor.front(); cor.pop();
int right = cor.front(); cor.pop();
ans.push_back({ left, right });
}
for (auto i : ans) printf("%d %d\n", i.first, i.second);
}
提示: 为了说明某种情况的不合法,可以考虑寻找必要条件。