[{"content":"何为康托展开 把一个自然数展开成 阶乘进制(factorial number system) 的数位序列。即将任意 x ∈ ℕ 唯一地写成\n$$ x = \\sum_{i \\geq 0} d_i \\cdot i!, \\qquad 0 \\leq d_i \\leq i $$那么我们就称 $(d_n,\u0026hellip;, d_2, d_1, d_0)$ 为 $x$ 的 康托展开(Cantor expansion)。\n例如：$463 = 3 × 5! + 4 × 4! + 1 × 3! + 0 × 2! + 1 × 1! + 0 × 0!$\n则 $463$ 的康托展开为 $(341010)$\nLehmer 码 设 $a = (a_1, a_2, \\dots, a_n)$ 是 ${1, 2, \\dots, n}$ 的一个排列。它的 Lehmer 码(Lehmer Code) 是一个序列\n$$ c = (c_1, c_2, \\dots, c_n) $$其中第 $i$ 位定义为：排在 $a_i$ 右边、且比 $a_i$ 小的元素个数，即\n$$ c_i = \\#\\{\\, j : j \u003e i,\\ a_j \u003c a_i \\,\\} $$由于 $a_i$ 右边最多只有 $n-i$ 个元素，所以\n$$ 0 \\leq c_i \\leq n - i $$特别地，$c_n = 0$ 恒成立。合法 Lehmer 码的个数为\n$$ n \\cdot (n-1) \\cdots 1 = n! $$正好等于排列总数，因此\n$$ \\text{排列} \\longleftrightarrow \\text{Lehmer 码} $$是一个双射。取值范围 $0 \\leq c_i \\leq n-i$ 恰好就是阶乘进制的数位约束，所以 Lehmer 码天然是阶乘进制的合法数位序列。\n字典序的排名 将 $a = (a_1, a_2, \\dots, a_n)$ 是 ${1, 2, \\dots, n}$ 的 $n!$ 个排列全部列举出来，将它们按照字典序升序排列，则某一个排列在这个序列中的位次就是这个排列的排名。通过这种方式，每一个长度为 $n$ 的排列都可以被唯一表示。因此这种做法可以用来对排列相关的问题进行状态压缩。\n而想要计算一个排列的排名，我们就可以利用上面的康托展开与Lehmer 码。\n由前文我们可以得知，每个排列都有一个 Lehmer 码和它唯一对应，我们将这个 Lehmer 码逆康托展开，也就是把 Lehmer 码当作阶乘进制的数位求值，就能得到排列按字典序的排名： $$ \\operatorname{rank}(a) = \\sum_{i=1}^{n} c_i \\cdot (n-i)! $$所以三者的关系是：\n$$ \\text{排列} \\xrightarrow{\\ \\text{取 Lehmer 码}\\ } (c_1, \\dots, c_n) \\xrightarrow{\\ \\text{按阶乘进制求值}\\ } \\text{排名} $$实现 拿排列 $\\rightarrow$ 排名的计算过程来举例子，首先我们需要得到 Lehmer 码。当然也可以 $O(n^2)$ 暴力，但是这种复杂度通常是不被接受的。因此我们还可以考虑利用树状数组来实现 $O(nlogn)$ 的计算方法:\n1 2 3 4 5 6 7 8 9 10 vector\u0026lt;int\u0026gt; toLehmer(vector\u0026lt;int\u0026gt;\u0026amp; p) { int n = p.size(); Fenwick\u0026lt;int\u0026gt; fen(n); vector\u0026lt;int\u0026gt; c(n); for (int i = n - 1; i \u0026gt;= 0; i--) { c[i] = fen.sum(p[i]); fen.add(p[i], 1); } return c; } 由此我们可以得到一个排列的 Lehmer 码，接下来就是通过阶乘进制来进行排名的计算了:\n1 2 3 4 5 6 7 8 9 10 i64 toRank(vector\u0026lt;int\u0026gt;\u0026amp; p) { int n = p.size(); auto c = toLehmer(p); i64 fac = 1, rk = 0; for (int i = n - 1; i \u0026gt;= 0; i--) { rk += c[i] * fac; fac *= (n - i); } return rk; } 同理我们可以得出通过排名来进行康托展开得到排列的方法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 vector\u0026lt;int\u0026gt; fromRank(int n, i64 rk) { vector\u0026lt;int\u0026gt; c(n); for (int i = 1; i \u0026lt;= n; i++) { c[n - i] = rk % i; rk /= i; } return fromLehmer(c); } vector\u0026lt;int\u0026gt; fromLehmer(vector\u0026lt;int\u0026gt;\u0026amp; c) { int n = c.size(); vector\u0026lt;int\u0026gt; p(n); Fenwick\u0026lt;AddMono\u0026lt;int\u0026gt;\u0026gt; fen(n); for (int i = 0; i \u0026lt; n; i++) { fen.add(i, 1); } for (int i = 0; i \u0026lt; n; i++) { p[i] = fen.kth(c[i]); fen.add(p[i], -1); } return p; } ","date":"2026-09-20T00:00:00Z","image":"/p/cantor/cover.png","permalink":"/p/cantor/","title":"浅谈康托展开"},{"content":"题意 有两棵以 $1$ 为根的有根树，分别有 $a$ 和 $b$ 个顶点，每个顶点有容量上限。有 $n$ 个物品，第 $i$ 个物品有美丽值，并且在第一棵树中位于顶点 $x_i$，在第二棵树中位于顶点 $y_i$。选择一个物品会占用两棵树中从对应顶点到根路径上所有节点的容量。要求恰好选择 $k$ 个物品，且两棵树每个节点的容量限制都不能被超过，求所选物品美丽值总和的最大值。\n建模 看到有题目具有容量上限和价值最大两个要求，于是考虑想到将题目建模为费用流：\n对于每一个饰品 $i$ ，我们连一条从 $x_i$ 指向 $y_i$ ，容量为 $1$ ，费用为 $-w_i$ 的有向边。容量为 $1$ 保证每个饰品只能被选择一次，费用取负用于把取最大值给转化成求最小费用。\n接着我们以一棵树的根为源点，另外一颗树的根为汇点进行建模，对于树上的边，我们可以将每个结点的容量转化为树边的容量，建一条容量为 $c_i$，费用为 $0$ 的边。\n跑费用流时，若我们最终得到的最大流无法达到 $k$ ，则题目无解。\n势能初始化 饰品边的费用为负，不能直接使用 Dijkstra。引入势能 $h$，将边权改为：\n$$ c'(u,v)=c(u,v)+h(u)-h(v) $$令 $W=\\max_i w_i$，源点和第一棵树的势能设为 $0$，第二棵树和汇点的势能设为 $-W$。\n这样，树边的新费用均为 $0$，饰品边的新费用为：\n$$ -w_i+0-(-W)=W-w_i\\ge0 $$因此所有初始有容量的边的新费用都非负，可以使用 Dijkstra。每轮最短路后，对可达点更新 $h(v)\\leftarrow h(v)+dist(v)$，即可继续增广。\nCode 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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; using i64 = int64_t; constexpr i64 inf = 1e18; template \u0026lt;typename T\u0026gt; struct MinCostFlow { struct Edge_ { int to; T cap; T cost; Edge_(int to_, T cap_, T cost_) : to(to_), cap(cap_), cost(cost_) {} }; int n; vector\u0026lt;Edge_\u0026gt; e; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; adj; vector\u0026lt;T\u0026gt; h, dist; vector\u0026lt;int\u0026gt; pre; bool dijkstra(int s, int t) { dist.assign(n, inf); pre.assign(n, -1); priority_queue\u0026lt;pair\u0026lt;T, int\u0026gt;, vector\u0026lt;pair\u0026lt;T, int\u0026gt;\u0026gt;, greater\u0026lt;pair\u0026lt;T, int\u0026gt;\u0026gt;\u0026gt; que; dist[s] = 0; que.emplace(0, s); while (!que.empty()) { auto [d, u] = que.top(); que.pop(); if (dist[u] != d) { continue; } for (auto\u0026amp; i : adj[u]) { int v = e[i].to; if (e[i].cap \u0026gt; 0 \u0026amp;\u0026amp; dist[v] \u0026gt; d + h[u] - h[v] + e[i].cost) { dist[v] = d + h[u] - h[v] + e[i].cost; pre[v] = i; que.emplace(dist[v], v); } } } return dist[t] != inf; } MinCostFlow() {} MinCostFlow(int n_) { init(n_); } void init(int n_) { n = n_; e.clear(); adj.assign(n, {}); h.assign(n, {}); } void addEdge(int u, int v, T cap, T cost) { adj[u].push_back(e.size()); e.emplace_back(v, cap, cost); adj[v].push_back(e.size()); e.emplace_back(u, 0, -cost); } void set(const vector\u0026lt;T\u0026gt;\u0026amp; potential) { h = potential; } pair\u0026lt;T, T\u0026gt; flow(int s, int t, T need = inf) { T flow = 0; T cost = 0; while (flow \u0026lt; need \u0026amp;\u0026amp; dijkstra(s, t)) { for (int i = 0; i \u0026lt; n; i++) { if (dist[i] != inf) { h[i] += dist[i]; } } T aug = need - flow; for (int i = t; i != s; i = e[pre[i] ^ 1].to) { aug = min(aug, e[pre[i]].cap); } for (int i = t; i != s; i = e[pre[i] ^ 1].to) { e[pre[i]].cap -= aug; e[pre[i] ^ 1].cap += aug; } flow += aug; cost += aug * h[t]; } return {flow, cost}; } struct Edge { int from; int to; T cap; T cost; T flow; }; vector\u0026lt;Edge\u0026gt; edges() { vector\u0026lt;Edge\u0026gt; a; for (int i = 0; i \u0026lt; e.size(); i += 2) { Edge x; x.from = e[i + 1].to; x.to = e[i].to; x.cap = e[i].cap + e[i + 1].cap; x.cost = e[i].cost; x.flow = e[i + 1].cap; a.push_back(x); } return a; } }; void solve() { int n, a, b, k; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; k; int S = 0, A = 1, B = A + a, T = B + b; MinCostFlow\u0026lt;i64\u0026gt; mcf(T + 1); for (int i = 0; i \u0026lt; a; i++) { int p, c; cin \u0026gt;\u0026gt; p \u0026gt;\u0026gt; c; p--; int from = (i == 0 ? S : A + p); int to = A + i; mcf.addEdge(from, to, c, 0); } for (int i = 0; i \u0026lt; b; i++) { int p, c; cin \u0026gt;\u0026gt; p \u0026gt;\u0026gt; c; p--; int from = B + i; int to = (i == 0 ? T : B + p); mcf.addEdge(from, to, c, 0); } i64 maxW = 0; for (int i = 0; i \u0026lt; n; i++) { int x, y; i64 w; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; w; x--, y--; maxW = max(maxW, w); mcf.addEdge(A + x, B + y, 1, -w); } vector\u0026lt;i64\u0026gt; p(T + 1, 0); fill(p.begin() + B, p.end(), -maxW); mcf.set(p); auto [flow, cost] = mcf.flow(S, T, k); cout \u0026lt;\u0026lt; (flow == k ? -cost : -1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin \u0026gt;\u0026gt; T; while (T--) { solve(); } return 0; } ","date":"2026-08-13T00:00:00Z","permalink":"/p/nc_summer_7_k/","title":"2026牛客多校第8场 K"},{"content":"在做圆方树相关例题的时候碰到了这个 trick (\n把点集里面的结点按照 $dfn$ 排序后，循环累加相邻两点的带权距离和，得到的就是边权和的两倍。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int k; cin \u0026gt;\u0026gt; k; vector\u0026lt;int\u0026gt; S(k); for (int i = 0; i \u0026lt; k; i++) { cin \u0026gt;\u0026gt; S[k]; S[k]--; } sort(S.begin(), S.end(), [\u0026amp;](int\u0026amp; x, int\u0026amp; y) { return dfn[x] \u0026lt; dfn[y]; }); i64 sum = 0; for (int i = 0; i \u0026lt; k; i++) { int u = S[i], v = S[(i + 1) % k]; sum += w[u] + w[v] - 2 * w[lca.get(u, v)]; } i64 ans = sum / 2; 如果要计算的是连通子图的结点总数，我们可以把每个点给转化为其父边的边权，然后用以上方法统计边权和即可得到结点总数，若要被统计的结点正好是连通子图中深度最浅的结点，则要将 $sum$ 加二，即答案 $ans$ 加一。\n1 2 3 if (lca.get(S[0], S[k - 1]) \u0026lt; n) { sum += 2; } ","date":"2026-08-12T00:00:00Z","permalink":"/p/edge_sum_of_subgraph/","title":"计算连通子图的边权和"},{"content":"题意 给定一条 $x$ 轴，上面分布着 $n$ 个点，每个点具有三个属性：$x_i$ , $w_i$ , $v_i$ ，分别刻画了点的坐标，初始奖励值和奖励值每分钟的衰减速率。我们从原点出发，每分钟移动一个单位，在时间 $t_i$ 第一次访问目标点时会获得奖励：$bonus_i = w_i - v_i t_i$ 。我们需要在至少访问所有目标点一次的前提下，使得我们访问目标点时得到的奖励值总和 $\\sum_{i=1}^n bonus_i$ 取得最大值。\n思路 我们设第一次访问到第 $i$ 个点时我们的总时间 $t_i$ ，于是我们可以注意到： $$ \\sum_{i=1}^n bonus_i = \\sum_{i=1}^n (w_i-v_i t_i)=\\sum_{i=1}^n w_i - \\sum_{i=1}^n v_i t_i $$ $\\sum_{i=1}^n w_i$ 是常数，因此问题等价于最小化惩罚值 $\\sum v_i t_i$。\n将点按 $x$ 坐标升序排序。在最优移动策略中，访问过的点在坐标轴上必然构成一个连续区间（因为来回跳跃只会增加未访问点的衰减时间）。因此，我们可以用区间 DP 来描述状态：设当前已经访问了区间 $[x_l, x_r]$ 内的所有点，且爱丽丝位于该区间的左端点 $x_l$ 或右端点 $x_r$。由于有两种情况，所以我们用两个 DP 数组来分别记录我们在这两点上时，当前惩罚值的最小值。\n我们定义：\n$fl_{l, r}$ ：我们在区间 $[x_l, x_r]$ 的左端点上，此时的惩罚值最小值。 $fr_{l r}$：我们在区间 $[x_l, x_r]$ 的右端点上，此时的惩罚值最小值。 接下来处理状态转移。例如，对于状态 $fl_{l, r}$ ，因为我们此时在区间左端点 $x_l$ ，所以我们有可能是从区间 $[x_{l + 1}, x_r]$ 的左端点向左转移而来，也就是 $fl_{l+1,r}$ ，也有可能是从区间 $[x_{l+1}, x_r]$ 的右端点向左转移而来，也就是 $fr_{l+1,r}$ 。那惩罚值怎么计算呢？可以发现我们已经通过 $fl$ 和$fr$ 记录了先前两种状态的最小惩罚值了，因此我们只需要累加上在转移过程中的新增惩罚值就行了，也就是用我们在转移过程中所花的时间，乘上未被覆盖的目标点的奖励值衰减速率总和（这一部分可以从过对起点和终点的坐标作差以及对每个目标点的奖励值衰减速率预处理一个前缀和来快速计算），我们把这段新增的从点 $x_i$ 到点 $x_j$ 的惩罚值简记为 $penalty_{i, j}$ 。 对于状态 $fr_{l,r}$ 也是同理。\n最后我们就可以得出状态转移方程： $$ \\begin{align} fl_{l,r} = \\min(fl_{l+1,r}+penalty_{l,l+1},fr_{l+1,r}+penalty_{l,r}) \\\\ fr_{l,r} = \\min(fl_{l,r-1}+penalty_{l,r},fr_{l,r-1}+penalty_{r-1,r}) \\end{align} $$ 其中 $penalty_{i, j} = [\\text{sumv} - (pre[r+1] - pre[l])] \\times (x_j - x_i)$ ，（前缀和数组使用左闭右开区间，其中的 $l$ 和 $r$ 为已访问区间的左右端点下标）\n我们从原点出发，但原点不一定恰好有点。为方便处理，我们在点集中添加一个原点 $(0, 0, 0)$ 表示起点。将该点与所有目标点一起排序，并令其所在下标 $pos$ 满足 $fl[pos][pos] = fr[pos][pos] = 0$，其余状态的初始值设为 $+\\infty$。\n最后我们得到的答案即为 $\\text{ans} = \\sum_{i=1}^n w_i - \\min(fl[0][n-1], fr[0][n-1])$\nCode 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 #include \u0026lt;bits/stdc++.h\u0026gt; using i64 = long long; constexpr i64 INF = 1E18; struct Point { i64 x, w, v; }; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int n; std::cin \u0026gt;\u0026gt; n; std::vector\u0026lt;Point\u0026gt; a(n); for (int i = 0; i \u0026lt; n; i++) { std::cin \u0026gt;\u0026gt; a[i].x \u0026gt;\u0026gt; a[i].w \u0026gt;\u0026gt; a[i].v; } Point zero; zero.x = 0, zero.w = 0, zero.v = 0; a.push_back(zero); n++; std::sort(a.begin(), a.end(), [](Point\u0026amp; x, Point\u0026amp; y) { return x.x \u0026lt; y.x; }); std::vector\u0026lt;i64\u0026gt; pre(n + 1); i64 sumv = 0, sumw = 0; for (int i = 0; i \u0026lt; n; i++) { pre[i + 1] = pre[i] + a[i].v; sumv += a[i].v; sumw += a[i].w; } std::vector fl(n, std::vector\u0026lt;i64\u0026gt;(n, INF)); std::vector fr(n, std::vector\u0026lt;i64\u0026gt;(n, INF)); for (int i = 0; i \u0026lt; n; i++) { auto v = a[i]; if (v.x == 0 \u0026amp;\u0026amp; v.w == 0 \u0026amp;\u0026amp; v.v == 0) { fl[i][i] = 0; fr[i][i] = 0; } } for (int l = n - 2; l \u0026gt;= 0; l--) { for (int r = l + 1; r \u0026lt; n; r++) { i64 ldt1 = a[l + 1].x - a[l].x; i64 ldt2 = a[r].x - a[l].x; i64 ldv = sumv - (pre[r + 1] - pre[l + 1]); i64 rdt1 = a[r].x - a[r - 1].x; i64 rdt2 = a[r].x - a[l].x; i64 rdv = sumv - (pre[r] - pre[l]); fl[l][r] = std::min(fl[l + 1][r] + (ldt1 * ldv), fr[l + 1][r] + (ldt2 * ldv)); fr[l][r] = std::min(fr[l][r - 1] + (rdt1 * rdv), fl[l][r - 1] + (rdt2 * rdv)); } } i64 ans = sumw - std::min(fl[0][n - 1], fr[0][n - 1]); std::cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } ","date":"2026-02-26T00:00:00Z","image":"/p/qed2503-j/cover.png","permalink":"/p/qed2503-j/","title":"QED2503J.黄袍加身非富贵"},{"content":"题目描述 给定一个长度为 $n$ 的 $01$ 串，选一个长度至少为 $L$ 的连续子串，使得子串中数字的平均值最大。如果有多解，子串长度应尽量小；如果仍有多解，起点编号尽量小。序列中的字符编号为 $1$ ~ $n$，因此 $[1,n]$ 就是完整的字符串。$1\\le n\\le 100000,1\\le L\\le 1000$。\n例如，对于如下长度为 $17$ 的序列00101011011011010，如果 $L=7$ ，最大平均值为 $\\frac 3 4$ （子序列为 $[7,14]$，其长度为 $8$）；如果 $L=5$，子序列 $[7,11]$ 的平均值最大，为 $\\frac 4 5$。\n输入格式 第一行输入 $T$，表示有 $T$ 组数据。\n每组数据的第一行输入两个正整数 $n$ 和 $L$。\n每组数据的接下来一行一个长度为 $n$ 的 $01$ 串。\n输出格式 输出选取的区间的左右端点。\n输入样例 1 2 3 4 5 2 17 5 00101011011011010 20 4 11100111100111110000 输出样例 1 2 7 11 6 9 分析 涉及到区间操作，我们首先想到用前缀和来储存并表示子串的数字之和，我们设前缀和为 $S_n$ ，则可以得到平均值的表达式：\n$$ avg_{i,j} = \\frac{S_j - S_{i-1}}{j-i+1} $$在这里我们要运用数形结合的思想，以字符串的下标 $i$ 在横坐标，$S_i$ 为纵坐标建系，则我们可以得到类似这样的图：\n而我们要找到 $avg_{i,j}$ 的最大值，即找到这个图像上斜率最大的割线，这时我们可以初步得到一个思路，就是枚举割线的右端点 $t$ ，然后再找到能使割线斜率最大的左端点。但是暴力枚举在数据量较大的时候是会 TLE 的，因此我们需要对目前的方法进行优化。\n首先，我们可以剔除那些显然不会使得斜率为最大值的左端点，我们该如何寻找他们？\n由上图可知，若我们右端点 $t$ 的横坐标为 $x_0$ ，则它有可能分布在上图的三个区域：$A$ 、$B$ 或 $C$ ，然而\n在 $A$ 处，$P_kt$ 的斜率显然大于 $P_jt$ 在 $B$ 处，$P_kt$ 和 $P_it$ 的斜率显然大于 $P_jt$ 在 $C$ 处，$P_it$ 的斜率显然大于 $P_jt$ 所以无论如何 $P_jt$ 的斜率都是最小的。\n综上我们可以得出，当图像上存在上凸点时，以它为左端点所得到的割线，它的斜率一定不是最大的，所以我们可以对图像进行预处理，把上凸点全部删掉，由于纵坐标是不递减的，故这个过程我们可以用单调栈来实现。即每往后添加一个新的点，我们都检测新的点与前两个点连线的斜率，如果斜率也是递增的，那么就不存在上凸点，否则我们就把前一个点（即上凸点），从栈中弹出。\n预处理之后，我们便可以来从 $L$ 到 $n$ 枚举点 $t$ ，从而找到斜率最大值。\n接下来就是第二个优化：对于每个点 $t$ ，我们不必枚举所有待选左端点。\n由此图我们可以得知，割线斜率的变化一定是先增后减的，因此我们只需要在斜率开始减小的时候直接 break 就行了。\n时间复杂度为 $O(n)$\n代码 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 #include \u0026lt;bits/stdc++.h\u0026gt; using i64 = long long; using u64 = unsigned long long; using u32 = unsigned; using pii = std::pair\u0026lt;int, int\u0026gt;; using pll = std::pair\u0026lt;i64, i64\u0026gt;; constexpr int maxn = 1e5 + 5; std::vector\u0026lt;int\u0026gt; a, pre; int stk[maxn]; int cmp_avg(int x1, int x2, int x3, int x4) { return ((pre[x2] - pre[x1 - 1]) * (x4 - x3 + 1) - (pre[x4] - pre[x3 - 1]) * (x2 - x1 + 1)); } void solve() { int n, L; std::string s; std::cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; L \u0026gt;\u0026gt; s; a.resize(n + 1, 0); pre.resize(n + 1, 0); for (int i = 1; i \u0026lt;= n; i++) a[i] = s[i - 1] - \u0026#39;0\u0026#39;; std::partial_sum(a.begin(), a.end(), pre.begin()); pii ans = {1, L}; int cur = 0, top = 0; for (int t = L; t \u0026lt;= n; t++) { while (top - cur \u0026gt; 1 \u0026amp;\u0026amp; cmp_avg(stk[top - 2], t - L, stk[top - 1], t - L) \u0026gt;= 0) top--; stk[top++] = t - L + 1; while (top - cur \u0026gt; 1 \u0026amp;\u0026amp; cmp_avg(stk[cur], t, stk[cur + 1], t) \u0026lt;= 0) cur++; int k = cmp_avg(stk[cur], t, ans.first, ans.second); if (k \u0026gt; 0 || (k == 0 \u0026amp;\u0026amp; t - stk[cur] \u0026lt; ans.second - ans.first)) { ans.first = stk[cur]; ans.second = t; } } std::cout \u0026lt;\u0026lt; ans.first \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; ans.second \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int T; std::cin \u0026gt;\u0026gt; T; while (T--) { solve(); } return 0; } ","date":"2025-12-07T00:00:00Z","image":"/p/uva1451/example2.jpg","permalink":"/p/uva1451/","title":"UVA1451 平均值 Average"},{"content":"题目链接：AtCoder ABC433 D\nProblem Statement For positive integers $x,y$, define $f(x,y)$ as follows:\nThe value obtained by interpreting $x,y$ in decimal notation without leading zeros as strings, concatenating them in this order to obtain a string $S$, and then interpreting $S$ as an integer in decimal notation. For example, $f(12,3)=123$ and $f(100,40)=10040$.\nYou are given positive integers $N,M$ and a sequence of $N$ positive integers $A=(A_1,A_2,\\ldots,A_N)$.\nFind the number of pairs of integers $(i,j)$ that satisfy all of the following conditions.\n$1\\le i,j\\le N$ $f(A_i,A_j)$ is a multiple of $M$. Constraints \\(1\\le N\\le 2\\times 10^5\\) \\(1\\le M\\le 10^9\\) \\(1\\le A_i\\le 10^9\\) All input values are integers. Input The input is given from Standard Input in the following format:\n1 2 N M A_1 A_2 \\ldots A_N Output Output the number of pairs of integers $(i,j)$ that satisfy all the conditions.\nSample Input 1 1 2 2 11 2 42 Sample Output 1 1 2 When $(i,j)=(1,1)$: $f(A_1,A_1)=22$ is a multiple of $11$. When $(i,j)=(1,2)$: $f(A_1,A_2)=242$ is a multiple of $11$. When $(i,j)=(2,1)$: $f(A_2,A_1)=422$ is not a multiple of $11$. When $(i,j)=(2,2)$: $f(A_2,A_2)=4242$ is not a multiple of $11$. Therefore, the valid pairs are $(i,j)=(1,1)$ and $(i,j)=(1,2)$, so the answer is $2$.\n分析 我们需要在数列 $A$ 中统计满足条件的有序数对 $(i,j)$：将 $A_i$ 和 $A_j$ 拼接后，所得的数是 $M$ 的倍数。\n设 $A_j$ 有 $k$ 位，则拼接后的结果可以表示为\n$$ f(A_i,A_j)=10^kA_i+A_j $$因此，$f(A_i,A_j)$ 能被 $M$ 整除，当且仅当\n$$ A_j\\equiv -10^kA_i\\pmod M $$我们按照数字的位数维护数组 $g$。其中，$g[k]$ 保存所有位数为 $k$ 的数对 $M$ 取模后的结果，并将每个数组排序。\n接着枚举每个 $A_i$。对于每一种可能的位数 $k$，计算目标余数\n$$ (M-10^kA_i\\bmod M)\\bmod M $$然后在 $g[k]$ 中用二分查找统计等于该余数的元素个数，并累加到答案中。\n代码 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 #include \u0026lt;bits/stdc++.h\u0026gt; using i64 = long long; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int N, M; std::cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; std::vector\u0026lt;int\u0026gt; A(N), mod(N); for (int\u0026amp; v : A) std::cin \u0026gt;\u0026gt; v; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; g(11); for (int v : A) g[std::to_string(v).size()].push_back(v % M); for (auto\u0026amp; gg : g) std::sort(gg.begin(), gg.end()); i64 ans = 0; for (i64 Ai : A) { for (int k = 1; k \u0026lt; 11; k++) { Ai *= 10; Ai %= M; int key = (M - Ai) % M; ans += std::lower_bound(g[k].begin(), g[k].end(), key + 1) - std::lower_bound(g[k].begin(), g[k].end(), key); } } std::cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } ","date":"2025-11-23T00:00:00Z","image":"/p/abc433-d/cover.jpg","permalink":"/p/abc433-d/","title":"ABC433 D：183183"},{"content":"前言 有意思的一个方法。 在紫书里面看到了一道题目，书中提到了一个叫做 $Polyomino$ 的方法，发现者是 $Redelmeier$ 。这个方法可以使得在枚举连通块种类的问题中避免枚举后去重，直接生成不会重复的几种连通块。\n基本思想 对于这多个连通块，我们首先要考虑该如何储存表示他们。 对于连通块的每一块，我们可以都用一个结构体 $Cell$ 来表示，而每个 $Cell$ 都包含了 $x$ 和 $y$ 两个成员用于储存他们的位坐标。那对于整个连通块我们就可以用一个包含了这个连通块的所有 $Cell$ 来表示，以下我们就称这样一个连通块为 $Polyomino$ ，为了寻找指定 $Cell$ 数量的连通块种类，我们可以用一个 $Polyomino$ 集合数组 $poly[n]$ 来储存 $Cell$ 数量为 $n$ 时的 $Polyomino$ 有哪些。\n1 2 3 4 5 6 7 8 9 10 11 12 13 #define FOR_CELL(c, p) for (Polyomino::const_iterator c = (p).begin(); c != (p).end(); c++) struct Cell { int x, y; Cell(int x = 0, int y = 0) : x(x), y(y) {}; bool operator\u0026lt;(const Cell\u0026amp; rhs) const { return x \u0026lt; rhs.x || (x == rhs.x \u0026amp;\u0026amp; y \u0026lt; rhs.y); } }; using Polyomino = std::set\u0026lt;Cell\u0026gt;; std::set\u0026lt;Polyomino\u0026gt; poly[MAXN + 1]; 接下来我们需要生成出各种符合要求且不重复的 $Polyomino$ ，其中生成这一步其实并不难，针对问题所要求的这种连通块个数以及大小限制，我们结合 $Polyomino$ 的元素数量和各个 $Cell$ 的坐标就能很快筛选出来，主要的难点就在于如何去重。\n生成 首先我们先将每个 $Polyomino$ 的第一个方块定义为坐标为 $(0, 0)$ 的 $Cell$ ，然后每次循环生成新的连通块的时候，我们都在前一组连通块的基础上进行生成。比如我们现在要生成元素数量为 $n$ 的连通块，那我们就遍历 $poly[n - 1]$ 中的所有连通块，在他们的基础上向四周各扩散一个块，检查当前的 $Polyomino$ 中是否有了扩散出来的 $Cell$ ，如果没有，再进行处理和插入。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 void generate() { Polyomino s; s.insert(Cell(0, 0)); poly[1].insert(s); for (int n = 2; n \u0026lt;= MAXN; n++) { for (std::set\u0026lt;Polyomino\u0026gt;::iterator p = poly[n - 1].begin(); p != poly[n - 1].end(); p++) FOR_CELL(c, *p) for (int dir = 0; dir \u0026lt; 4; dir++) { Cell newc(c-\u0026gt;x + dx[dir], c-\u0026gt;y + dy[dir]); if (p-\u0026gt;count(newc) == 0) check_polyomino(*p, newc); } } } 处理 接下来就是最核心的步骤，即对我们新生成出来的 $Polyomino$ 进行处理，在这里我们要用到一个标准化的思想，即对所有的的 $Polyomino$ 进行一定的处理，以将其化为一个标准的状态。\n我们前面已经知道了，$Polyomino$ 是由一个个带有坐标的 $Cell$ 组成的，而在生成的过程中，也许两个全等的 $Polyomino$ 在坐标平面上的位置并不同，而将他们全部处理成相同的一种形式就是标准化的过程。\n1 2 3 4 5 6 7 8 9 10 11 Polyomino normalize(const Polyomino\u0026amp; p) { int minX = p.begin()-\u0026gt;x, minY = p.begin()-\u0026gt;y; FOR_CELL(c, p) { minX = std::min(minX, c-\u0026gt;x); minY = std::min(minY, c-\u0026gt;y); } Polyomino p2; FOR_CELL(c, p) p2.insert(Cell(c-\u0026gt;x - minX, c-\u0026gt;y - minY)); return p2; } 形象化地解释一下，就是把我们当前的这个 $Polyomino$ 平移到第一象限，然后让 $Polyomino$ 最左边和最下边的 $Cell$ 紧贴着 $x$ 和 $y$ 的正半轴。\n处理好了位置，接下来就是如何使得旋转和翻转后的 $Polyomino$ 不被计入，首先我们先定义以下 $Polyomino$ 的旋转和翻转这两个过程是怎么完成的。\n旋转：顺时针旋转 $90°$ ，也就意味着原本的纵坐标变成了如今的横坐标，而原本的横坐标的相反数变成了如今的纵坐标，所以我们就有\n1 2 3 4 5 6 Polyomino rotate(const Polyomino\u0026amp; p) { Polyomino p2; FOR_CELL(c, p) p2.insert(Cell(c-\u0026gt;y, -c-\u0026gt;x)); return normalize(p2); } 反转：将 $Polyomino$ 上下反转，则横坐标不变，纵坐标变为原来的相反数。\n1 2 3 4 5 6 Polyomino flip(const Polyomino\u0026amp; p) { Polyomino p2; FOR_CELL(c, p) p2.insert(Cell(c-\u0026gt;x, -c-\u0026gt;y)); return normalize(p2); } 接下来就是插入的操作。 我们在插入前，先将 $Polyomino$ 标准化，然后用 $for$ 循环将它旋转 $4$ 次，每次都用 $count()$ 检测集合 $poly[n]$ 里面是否有当前形式的的 $Polyomino$ ，如果有，直接 $return$ 。 如果没有，我们再将 $Polyomino$ 进行反转，再次进行上述操作。当八次操作全都没在集合中查询到一样的 $Polyomino$ 时，我们就可以把它插入到 $poly[n]$ 里面了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void check_polyomino(const Polyomino\u0026amp; p0, const Cell\u0026amp; c) { Polyomino p = p0; p.insert(c); p = normalize(p); int n = p.size(); for (int i = 0; i \u0026lt; 4; i++) { if (poly[n].count(p) != 0) return; p = rotate(p); } p = flip(p); for (int i = 0; i \u0026lt; 4; i++) { if (poly[n].count(p) != 0) return; p = rotate(p); } poly[n].insert(p); } 这就是在生成和插入阶段我们所做的操作，接下来就是统计\n统计 因为题目要求我们根据 $Polyomino$ 的块数和能容纳下它的矩形的面积进行筛选，因为我们前面对 $Polyomino$ 进行了标准化，所以我们可以找到 $Polyomino$ 的横纵坐标最大值来确定它的“矩形面积”，从而进行筛选。\n1 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 void generate() { Polyomino s; s.insert(Cell(0, 0)); poly[1].insert(s); for (int n = 2; n \u0026lt;= MAXN; n++) { for (std::set\u0026lt;Polyomino\u0026gt;::iterator p = poly[n - 1].begin(); p != poly[n - 1].end(); p++) FOR_CELL(c, *p) for (int dir = 0; dir \u0026lt; 4; dir++) { Cell newc(c-\u0026gt;x + dx[dir], c-\u0026gt;y + dy[dir]); if (p-\u0026gt;count(newc) == 0) check_polyomino(*p, newc); } } for (int n = 1; n \u0026lt;= MAXN; n++) for (int w = 1; w \u0026lt;= MAXN; w++) for (int h = 1; h \u0026lt;= MAXN; h++) { int cnt = 0; for (std::set\u0026lt;Polyomino\u0026gt;::iterator p = poly[n].begin(); p != poly[n].end(); p++) { int maxX = 0, maxY = 0; FOR_CELL(c, *p) { maxX = std::max(maxX, c-\u0026gt;x); maxY = std::max(maxY, c-\u0026gt;y); } if (std::min(maxX, maxY) \u0026lt; std::min(h, w) \u0026amp;\u0026amp; std::max(maxX, maxY) \u0026lt; std::max(h, w)) ++cnt; } ans[n][w][h] = cnt; } } 总结 这个方法的特殊点在于它很巧妙地用了一个“标准化”的思想来对 $Polyomino$ 进行处理，从而使得我们的后续处理变得非常方便。\n","date":"2025-11-10T00:00:00Z","image":"/p/polyomino/example2.jpg","permalink":"/p/polyomino/","title":"Polyomino"},{"content":"一. Vector 动态数组，在内存中具有连续的储存空间，在堆上分配内存，支持快速随机访问，在中间插入和删除慢，但在末尾插入和删除快。\n时间复杂度分析： 头部插入删除：$O(N)$\n尾部插入删除: $O(1)$\n中间插入删除：$O(N)$\n查找：$O(N)$\n适用场景：适用于元素结构简单，变化小，并且频繁随机访问的场景。\n二.Deque 是一个动态数组，可以向两端发展(双向开口的连续线性空间)，因此无论在头部或者尾部安插元素都十分迅速，在中间按插元素则比较费时，因为必须移动其他元素。\n双端队列的元素被表示为一个分段数组，容器中的元素分段存放在一个个大小固定的数组中。\n由于分段数组的大小是固定的，并且他们的首地址被连续存放在索引数组中，因此可以对其进行随机访问，但是效率比vector低很多。\n向两端加入新元素时，如果这一端的分段数组未满，则可以直接加入，如果这一端的分段数组已满，只需要创建新的分段数组，并把该分段数组的地址加入到索引数组中即可，这样就不需要对已有的元素进行移动，因此在双端队列的两端加入新的元素都具有较高的效率。\n当双端队列删除首尾元素时，也不需要移动，所以效率也比较高。\n双端队列中间插入元素时，需要将插入点到某一端之间的所有元素向容器的这一端移动，因此向中间插入元素的效率较低，而且往往插入的位置越靠近中间，效率越低，删除队列中元素时，情况也类似。\n时间复杂度分析 头部尾部插入删除：$O(1)$\n中间插入删除：$O(N)$\n查找：$O(N)$\n适用场景：适用于既要频繁随机储存，又要关心两端数据的插入与删除操作，并且不需要频繁对中间元素进行插入删除的场景。\n三.双向链表list list由双向链表实现，元素存放在堆中，每个元素都是存放在一块内存中，它的内存空间可以是不连续的，通过指针来进行数据访问，这个特点使得它的随机储存变得非常的没有效率，但是由于链表的特点，它可以很有效率的支持任意地方的插入和删除操作。\n时间复杂度分析 任何位置的插入删除：$O(1)$ (假设告诉了你插入删除的位置，不需要你线查找再删除)\n头尾查询：$O(1)$\n其他位置查询：$O(N)$\n适用场景：适用于经常进行插入和删除并且不经常随机访问的场景。\n四.set set由红黑树实现，其内部元素依照其值自动排序，每个元素只出现一次，不允许重复(红黑树是平衡二叉树的一种)\n时间复杂度分析 增删改查近似：O(log N)\n适用场景：适用与经常查找一个元素是否在某集群中并且不要排序的场景\n五. multiset multiset和set相同，只不过它允许重复元素，也就是说multiset可包括多个数值相同的元素。这里不再做过多介绍。\n六. map map由红黑树实现，其元素都是键值对，每个元素的键是排序的准则，每个键只能出现一次，不允许重复\n时间复杂度分析 增删改查基本是O(log N)\n适用场景：适用于需要储存一个字典，并要求方便的根据key找value的场景\n七. multimap multimap 和 map 相同，但允许重复元素，也就是说multimap可包含多个键值（key）相同的元素。这里不再做过多介绍。\n📝 备注 stack，queue，prioroty_queue都属于容器配接器，是由容器按照特殊的逻辑实现的\n小结：\n如果需要高效的随机存取，不在乎插入和删除的效率，使用vector。 如果需要大量的插入和删除元素，不关心随机存取的效率，使用list。 如果需要随机存取，并且关心两端数据的插入和删除效率，使用deque。 如果打算存储数据字典，并且要求方便地根据key找到value，一对一的情况使用map，一对多的情况使用multimap。 如果打算查找一个元素是否存在于某集合中，唯一存在的情况使用set，不唯一存在的情况使用multiset。** ","date":"2025-09-29T00:00:00Z","permalink":"/p/cpp_stl/","title":"C++ STL 常见容器查找、删除和增添的时间复杂度"}]