## 排队 #枚举 #数学 > [!note] 思路 > > 首先考虑如何支持等差,不难想到枚举差 $k$ 然后在此方差的情况下选择修改次数最小的方案。由于它规定了值域是 $[1,w]$,而方差为 $k$ 时目标 $a_n$ 至少为 $1+(n-1)k$,所以枚举上界就是 $k=\left\lfloor\cfrac{w-1}{n-1}\right\rfloor$。这个枚举时间复杂度 ${} \mathcal O\left(\cfrac{w}{n}\right) {}$。 > > 对于第 $i$ 个元素,我们生成基准目标元素 $a_i=1+(i-1)k$,那么需要将原始 $a$ 修改成和基准目标元素每个差值相同的(保证插值 $k$)。具体来说,选择与基准元素差值相同数量最多的,最后需要修改的数量就越少。 > > 这个检查是 $\mathcal O(n)$ 线性的。但是由于需要用到负数下标,我使用了 `map`。 > [!attention] 注意 > > 需要特别注意第一个元素和最后一个元素,不能超过 $[1,w]$。 > > > [!bug] Hack 数据 > > 对于 $n=4,m=7$,有 $a=(2,4,6,7)$。 > > > > 答案是 $2$ 不是 $1$,因为最后一个元素无法别改成 $8$。 时间复杂度 $\mathcal O\left(\cfrac{w}{n}\times n\log_2 n\right)=\mathcal O\left(w\log_2 n\right)$。 ```cpp const LL kMaxN = 3e5 + 5; LL a[kMaxN], n, m, ans = 1e18; map<LL, LL> f; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m; for (LL i = 1; i <= n; i++) { cin >> a[i]; } for (LL k = 0; 1 + (n - 1) * k <= m; k++) { f.clear(); for (LL i = 1; i <= n; i++) { int d = 1 + (i - 1) * k - a[i]; if (1 - d >= 1 && 1 + (n - 1) * k - d <= m) { f[d]++; } } for (auto i : f) { ans = min(ans, n - i.second); } } cout << ans << '\n'; return 0; } ``` ## 攀比 #结论 #找规律 > [!note] 思路 > > 这道题目有多个询问,这代表着我们可能需要 $\mathcal O(\log)$ 级别求出单次询问的答案。 > > 首先考虑找规律/猜结论。对于一个点 $x$,我们删掉之后这个连通块会变成多少个呢?当然是 $d_x$ 个;然而,由于 $x$ 是之前更小的点删去后分出来的连通块的新的最小点,所以实际上是变成 $d_x-1$ 个,但是对于一个初始连通块本身就有一个。所以答案就是连通块数量+$\sum \max(0,d_i-1)$……吗? > > 随便画一个不是树的连通块,就发现一个点删掉之后并不会出现 $d_x-1$ 个连通块。现在我们知道从连通块个数的增加上考虑思路有点困难了,不如直接考虑最后不被删除需要什么条件。 > > 思考两年半,我们可以得知一个节点如果最后会留下来,那么它的邻居节点的编号一定均小于它,因为如果有大于它的它就会因为存在于一个大小大于 $1$ 的连通块中删除。 > > 所以我们直接用 `set` 动态维护每个节点的邻点编号,动态计算答案即可。 时间复杂度 $\mathcal O(q\log_2 n)$,当然可以通过标记的方式去掉 `set` 的 $\log$。 ```cpp const int kMaxN = 1e5 + 5; int n, m, q, ans; set<int> e[kMaxN]; bool check(int x) { return e[x].empty() || *e[x].rbegin() < x; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m; for (int i = 1, u, v; i <= m; i++) { cin >> u >> v; e[u].insert(v); e[v].insert(u); } for (int i = 1; i <= n; i++) { ans += check(i); } cin >> q; for (int o, u, v; q; q--) { cin >> o; if (o == 1) { cin >> u >> v; ans -= check(u); ans -= check(v); e[u].insert(v); e[v].insert(u); ans += check(u); ans += check(v); } else if (o == 2) { cin >> u >> v; ans -= check(u); ans -= check(v); e[u].erase(v); e[v].erase(u); ans += check(u); ans += check(v); } else { cout << ans << '\n'; } } return 0; } ``` ## 排排队 #暴力 #找规律 #结论 #前缀和 > [!important] 重点 > > 这种题目又出现了,就是优化枚举。普通的暴力枚举肯定过不了,但是对于大多数情况都不可能造成最优解的,直接不考虑。 > [!note] 思路 > > 若干次合并相邻的两个元素可以看作区间合并为一个,于是想到异或前缀和,这样就可以 $\mathcal O(1)$ 求出区间异或和了。 > > 一个显而易见的结论是: > > - [n] 我们只会进行区间合成最多两次,因为只需要构造一对逆序的。 > > 于是可以想出时间复杂度为 $\mathcal O(n^3)$ 的暴力,即枚举左端点 $i$ 和右端点 $j$,中间枚举 $k$ 让 $[i,k]$ 的合并,$[k+1,j]$ 的合并,满足的要求就是 $p_k\oplus p_{i-1}=p_j\oplus p_{k}$。答案取 $\min(j-i-1)$。 > > 对于 $n>400$,直接输出 $1$。此时就可以直接获得 $100$ 分了。 > > > [!question] 疑问 > > 为什么这样做是对的呢?怎么想到的?具体的边界真的是 $400$ 还是其它? > > > [!tldr] 证明 > > > > 实际上边界是 $60$。 > > > > 对于 $3$ 个相邻的数,如果它们最高 $1$ 位是相同的,那么前两个异或之后这一位就没有了,所以答案是 $1$。 > > > > 为了避免这种情况,我们需要构造相邻 $3$ 个数最高位均不同的,但是最高位不能单调递减(这样本身答案就是 $0$ 了)。所以最好的构造方案是每个数位作为最高位构造两个数,最高数位单调递增。这样子就是 $60$ 个数了。大于 $60$ 必然崩盘,答案就为 $1$。 > > 我的做法更奇特。没有钦定对于 $n>60$ 答案一定为 $1$,因为不保险。我钦定答案不会超过 $2\times 30=60$,所以对于每个 $i$,$j$ 只枚举往后最多 $60$ 个。 > > > [!attention] 注意 > > > > 这种做法的运算量高达 $7.2\times 10^8$,需要配合最优性剪枝才能通过。当然忽略常数时间复杂度 $\mathcal O(n)$。 这是我的做法。 ```cpp const int kMaxN = 2e5 + 5; int p[kMaxN], a[kMaxN], t, n, ans; int main() { cin.tie(0)->sync_with_stdio(0); for (cin >> t; t; t--) { cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; p[i] = p[i - 1] ^ a[i]; } ans = 1e9; for (int i = 1; i <= n; i++) { for (int j = i + 2; j <= min(n, i + 60); j++) { if (j - i - 1 >= ans) { break; } for (int k = i; k < j; k++) { if ((p[k] ^ p[i - 1]) > (p[j] ^ p[k])) { ans = j - i - 1; } } } } cout << (ans != 1e9 ? ans : -1) << '\n'; } return 0; } ``` ## 南斯拉夫 #深度优先搜索 #广度优先搜索 #并查集 > [!note] 思路 > > 我采用的暴力。 > > 首先可以看作二选一,每个元素最后必须被选偶数次,于是 DFS 暴搜 $\mathcal O(2^mn)$,$\mathcal O(n)$ 判断优化为 $\mathcal O(m)$ 判断后直接获得 $40$ 分。