个人赛题解
A. Rectangular Queries
题意
给定一个最多包含 \(10\) 个不同数字的 \(n\times n\) 矩阵,依次回答 \(q\) 个询问,每个询问给定了一个子矩阵,问该子矩阵有多少不同数字。
题解
注意只有十个数字,预处理二维前缀和数组\(cnt[k][i][j]\)表示子矩阵 \((1,1) \to (i,j)\)中数字\(k\)的个数,每次询问枚举数字\(k\),通过作差即可得到子矩阵内数字 \(k\)的个数,进而知道有多少个不同数字。
时间复杂度为 \(O(10(n^2 + q))\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 300 + 5;
int a[N][N];
int cnt[N][N][11];
int main(void)
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t = 1;
while (t--) {
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) {
cin >> a[i][j];
for (int k = 0; k < 10; ++k)
cnt[i][j][k] = 0;
++cnt[i][j][a[i][j]];
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
for (int k = 0; k <= 10; ++k) {
cnt[i][j][k] += cnt[i][j - 1][k] + cnt[i - 1][j][k] - cnt[i - 1][j - 1][k];
}
}
}
int q;
cin >> q;
while (q--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
int ans = 0;
for (int i = 0; i <= 10; ++i) {
int ll = cnt[x2][y2][i] - cnt[x1 - 1][y2][i] - cnt[x2][y1 - 1][i] + cnt[x1 - 1][y1 - 1][i];
ans += (ll > 0);
}
cout << ans << '\n';
}
}
return 0;
}
B. Killing Monsters
题意
给定一个包含\(n\)个正整数的数组\(a\),以及 \(q\)次操作,第 \(i\)个操作包含 \(x_i,y_i\),表示将数组 \(a\)中下标满足 \(j \& x_i = j\)的 \(a_j = a_j - y_i\)。问每次操作后数组\(a\)的正数元素个数。
题解
\(a\&b=a\)意味着二进制下 \(a\)的所有 \(1\)的位置, \(b\)在该位置下都是 \(1\)。
枚举 \(b\)的子集的方式
for(int i = b; i >= 0; i = (i ? ((i - 1) & b) : i - 1));
分块暴力
将\(q\)次操作分块,每 \(\sqrt{q}\)个操作为一块,每一块中预处理该块的所有操作对数组 \(a\)的所有数的影响。
预处理的方法就是
for (int i = 0; i < cnt; ++i) { // cnt 块数
for (int j = sz * i, up = min(sz * (i + 1), q); j < up; ++j) { // sz 每块的操作数, q 询问数
blk[i][x[j] & MARK] += y[j]; // MARK 就是 (1 << 17) - 1
}
for (int j = 0; j < 18; ++j) { // 枚举1的位置
for (int k = 0; k <= MARK; ++k) { // 高往低传递y
if ((k >> j) & 1)
blk[i][k ^ (1 << j)] += blk[i][k];
}
}
} // blk[i][j]表示第i块的所有操作对第j个数的减去的值
然后考虑数组 \(a\)的每一个元素\(a_i\),求出它在哪次操作后变为非正数。依次遍历每个块,直到经过某一块时 \(a_i\)会变成非整数,然后再暴力遍历该块的每个操作,得到变成非正数的操作序号。
记答案数组\(ans[i]\)表示第 \(i\)次操作后的正数数量,对于第\(i\)个元素 \(a_i\),如果它在第\(dead_i\)次操作变成非正数,那么\(ans[1..dead_i-1]\)都要加 \(1\)。换个方向说,一开始有 \(ans\)数组的值全为\(n\),那么 \(ans[dead_i...q]\)都要减一。用差分思想更新\(ans\)数组,最后求遍前缀和还原该数据得到答案。
时间复杂度为\(O(n\sqrt{q}\log n + n\sqrt{q})\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = (1 << 17) + 8;
const int Q = (1 << 18) + 8;
const int MARK = (1 << 17) - 1;
const int SQRT = ceil(sqrt(Q));
LL blk[SQRT][MARK + 1];
int n, q, sz, cnt;
LL h[N];
int x[Q], y[Q];
int ans[Q];
int main(void)
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 0; i < n; ++i)
cin >> h[i];
cin >> q;
sz = sqrt(q);
cnt = (q + sz - 1) / sz;
for (int i = 0; i < q; ++i) {
cin >> x[i] >> y[i];
}
for (int i = 0; i < cnt; ++i) {
for (int j = sz * i, up = min(sz * (i + 1), q); j < up; ++j) {
blk[i][x[j] & MARK] += y[j];
}
for (int j = 0; j < 18; ++j) {
for (int k = 0; k <= MARK; ++k) {
if ((k >> j) & 1)
blk[i][k ^ (1 << j)] += blk[i][k];
}
}
}
for (int i = 0; i < n; ++i) {
int cur = 0;
while (cur < cnt) {
if (h[i] <= blk[cur][i])
break;
else {
h[i] -= blk[cur][i];
++cur;
}
}
if (cur >= cnt)
continue;
int pos = cur * sz;
int up = min(q, (cur + 1) * sz);
while (pos < up) {
if ((i & x[pos]) == i) {
if (h[i] <= y[pos]) {
ans[pos + 1]--;
break;
} else {
h[i] -= y[pos];
}
}
++pos;
}
}
ans[0] = n;
for (int i = 1; i <= q; ++i) {
ans[i] += ans[i - 1];
cout << ans[i] << '\n';
}
return 0;
}
整体二分
对于第 \(i\)个元素 \(a_i\),注意到它 在第几次操作后变成非正数存在单调性(假设它在第\(j\)次操作后变成非正数,那么对于函数,自变量是第几次操作,因变量是是否是非负数(true/false),这是个单调函数)。因此对于一个元素我们可以二分求其解。
因为所有元素都可以这样二分其操作次数求解,而如果我们对每一个元素单独二分求解,期间必定有重复计算操作。
为降低复杂度,我们只进行一次二分,每次根据二分出来的值,将每个元素的求解归到较小的范围继续二分或者较大的范围继续二分,这就是整体二分。
二分之后就是要执行\(l\to mid\)的操作,这里涉及子集加和单点查询,常规处理方式的复杂度是\(O(qn)\):
- 记\(dp[i]\)表示下标 \(i\)加的数,那么查询的复杂度是 \(O(n)\),子集加的复杂度是 \(O(1)\),总的复杂度就是\(O(qn)\)
- 记\(dp[i]\)表示下标 \(i\)及其\(i\)的超集加的数的和,那么查询的复杂度是 \(O(1)\),子集加的复杂度是 \(O(n)\),总是复杂度还是\(O(qn)\)
我们采用折中的方法,设\(i\)的高9位是 \(up_i\), \(i\)的低9位是 \(down_i\),\(dp[i]\) 表示所有操作下标\(x_j\)满足: \(up_{x_j} \& up_i = up_i, down_{x_j} = down_i\)的所有\(y_j\)的和(即记一半、查一半)。这样子集加和单点查询的复杂度都降为\(O(\sqrt{n})\),总的复杂度就降为\(O(q\sqrt{n})\)
最后得到每个元素在第几次操作变成非正数后,同样用差分求出答案数组。
总的时间复杂度为\(O(q\sqrt{n} \log q)\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = (1 << 17) + 8;
const int Q = (1 << 18) + 8;
const int DOWN = (1 << 9) - 1;
const int UP = (DOWN << 9);
int n, q;
LL h[N];
int id[N];
int tmp[N];
int ans[Q];
LL damage[N];
struct query{
int x;
LL y;
}op[Q];
void add(int pos, LL val){
int high = (pos & UP), low = (pos & DOWN);
for(int i = high; i; i = ((i - 1) & high)){
damage[i | low] += val;
}
damage[low] += val;
}
LL query(int pos){
LL sum = 0;
int high = (pos & UP), low = (pos & DOWN), inv = (low ^ DOWN);
for(int i = inv; i; i = ((i - 1) & inv)){
sum += damage[high | low | i];
}
sum += damage[high | low];
return sum;
}
void solve(int l, int r, int L, int R){
if (L > R)
return;
if (l == r){
for(int i = L; i <= R; ++ i){
int cur = id[i];
if ((cur & op[l].x) == cur && h[cur] <= op[l].y){
ans[l] --;
}
}
return;
}
int mid = (l + r) >> 1;
int p = L, q = R;
for(int i = l; i <= mid; ++ i){
add(op[i].x, op[i].y);
}
for(int i = L; i <= R; ++ i){
int cur = id[i];
LL sum = query(cur);
if (sum >= h[cur]){
tmp[p++] = cur;
}
else{
tmp[q--] = cur;
h[cur] -= sum;
}
}
for(int i = L; i <= R; ++ i)
id[i] = tmp[i];
for(int i = l; i <= mid; ++ i){
add(op[i].x, -op[i].y);
}
solve(l, mid, L, p - 1);
solve(mid + 1, r, q + 1, R);
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 0; i < n; ++ i)
cin >> h[i];
cin >> q;
for(int i = 1; i <= q; ++ i)
cin >> op[i].x >> op[i].y;
iota(id + 1, id + 1 + n, 0);
solve(1, q, 1, n); // [1, q]
ans[0] = n;
for(int i = 1; i <= q; ++ i){
ans[i] += ans[i - 1];
cout << ans[i] << '\n';
}
return 0;
}
C. Subarray Weight
题意
给定一个\(n\)的全排列数组 \(a\),求所有 \(a\)的子数组的权值和。
一个数组的权值定义为该数组的最大值和最小值的下标(相对原数组的下标)的差的绝对值。
题解
较快的暴力
可以事先花\(O(n\log n)\)或者 \(O(n^2)\)的时间预处理出每个子数组的最大值和最小值的下标,然后花 \(O(n^2)\)枚举子数组的左右端点,求其权值和。时间复杂度是 \(O(n^2)\),很显然这是不够的。
分治
中间切一刀,答案就由三部分组成,子数组在左半边、子数组在右半边、子数组横跨中间。对于前两者我们可以递归求解,考虑第三者如何求解。
从中间位置 \(mid\)开始往左枚举左端点,此时左半部分的最小值会越来越小,最大值会越来越大。(单调)
现在左边枚举了一个左端点,考虑右端点从中间位置 \(mid\)不断往右边移动,其子数组的权值。
注意到右端点从中间位置往右移动时,右半部分的最小值同样越来越小,最大值会越来越大。
注意到左右两半部分都有最小值和最大值,考虑全局的最大值和最小值的位置,在右端点往右移动的过程,有三个阶段:
- 全局的最小值和最大值都在左半部分
- 最小值和最大值,一个在左半部分,一个在右半部分
- 最小值和最大值都在右半部分
对于第一个阶段,极值下标差是固定的,我们只需要统计这样的右端点的数量,乘以差的绝对值即可。
对于第二个阶段,由于左半边极值下标固定,右半边极值下标不固定,但我们已知其大小关系,因此可以事先预处理出下标的前缀和求解。
对于第三个阶段,由于两个极值的都在右半边不固定,但我们可以通过预处理得到结果。
事先预处理 五个数组,分别为:
- \(minr[i]\)表示 \([mid,i]\)的最小值的下标
- \(maxr[i]\)表示 \([mid,i]\)的最大值的下标
- \(sminr[i]\)表示 \(minr[mid,i]\)的和
- \(sminr[i]\)表示 \(maxr[mid,i]\) 的和
- \(sans[i]\)表示 子数组左端点在\(mid\),右端点在 \(mid...i\)的权值和(事实上 \(sans[i] = sans[i - 1] + abs(maxr[i] - minr[i])\)) 该数组用于求解第三阶段的值
因为随着左端点的枚举,左半部分的极值只会单调变化。
因此阶段划分的两个位置也会不断往右边移动,因此双指针法维护该阶段划分点即可(实际左边一个指针,右边两个指针)
总时间复杂度为\(O(n\log n)\),是可以通过的
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 5e5 + 8;
LL a[N], minn[N], maxx[N], sminn[N], smaxx[N], spul[N], ans;
int n;
void fz(int l, int r)
{
if (r < l)
return;
if (l == r) {
return;
}
int mid = (l + r) >> 1;
fz(l, mid);
fz(mid + 1, r);
minn[mid] = mid;
maxx[mid] = mid;
sminn[mid] = 0;
smaxx[mid] = 0;
spul[mid] = 0;
for (int i = mid + 1; i <= r; ++i) {
if (i == mid + 1 || a[i] > a[maxx[i - 1]])
maxx[i] = i;
else
maxx[i] = maxx[i - 1];
if (i == mid + 1 || a[i] < a[minn[i - 1]])
minn[i] = i;
else
minn[i] = minn[i - 1];
sminn[i] = sminn[i - 1] + minn[i];
smaxx[i] = smaxx[i - 1] + maxx[i];
spul[i] = (spul[i - 1] + abs(maxx[i] - minn[i]));
}
LL mi = mid;
LL ma = mid;
LL mis = mid;
LL mas = mid;
for (int i = mid; i >= l; i--) {
if (a[i] < a[mis])
mis = i;
if (a[i] > a[mas])
mas = i;
while ((mi < r) && (a[minn[mi + 1]] >= a[mis]))
mi++;
while ((ma < r) && (a[maxx[ma + 1]] <= a[mas]))
ma++;
if (mi < ma)
ans =
(ans + abs(mis - mas) * (mi - mid) + (sminn[ma] - sminn[mi]) - mas * (ma - mi) + (spul[r] - spul[ma]));
else
ans =
(ans + abs(mis - mas) * (ma - mid) + (smaxx[mi] - smaxx[ma]) - mis * (mi - ma) + (spul[r] - spul[mi]));
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--) {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
ans = 0;
fz(1, n);
cout << ans << '\n';
}
return 0;
}
解法2
这个解法貌似没用到什么特别的算法。
观察权值的计算方式\(|i-j|\),是下标给答案有贡献,那么我们尝试能否计算出每个下标对答案的贡献,这样所有下标的贡献加起来,就是答案。
依次考虑每个下标分别作为最小值和最大值的贡献。
首先可以用单调栈以\(O(n)\)的时间复杂度求出每个数的左右两边第一个比它大(小)的数的位置。
对于一个下标 \(mid\),我们假设它是最大值(最小值同样的道理),其左右两边第一个比它大的数的位置在 \(l, r\),那么所有子数组,其左右端点 \(L, R\)满足 \(l < L \leq mid \leq R < r\) 的最大值都是\(a[mid]\)
然后考虑最小值的位置。因为要把绝对值去掉,因此需要分别考虑最小值在最大值左边还是右边的情况。
如果有\(lcnt\)个子数组最小值位置在 \(mid\)的左边(此时\(mid - min\)),有 \(rcnt\)个在右边(此时\(min - mid\)),那么这个下标作为最大值对答案的贡献就是 \((lcnt-rcnt) \times mid\)
现在思考如何求出\(lcnt,rcnt\)。
为保证最后做法的时间复杂度,我们假设\(mid - l < r - mid\),此时应枚举左端点(反之枚举右端点)
考虑从\(mid, mid - 1, ..., l + 1\)枚举左端点\(lp\),此时有一个左半部分的最小值\(min\)。然后考虑右端点\(rp\)依次从 \(mid\)到 \(r-1\)移动,此时在右端点应有第一个小于\(min\)的位置,记为 \(rpos\),此位置可从一开始用单调栈得到。
对于任何左端点是\(lp\),右端点\(rp < rpos\)的子数组,最小值都在 \(mid\)左边,而右端点\(rp \geq rpos\),最小值在 \(mid\)的右边。
此时以 \(lp\)为左端点的数组中,右端点从 \(mid \to r - 1\),共有 \(rpos - mid\)个子数组,最小值在 \(mid\)左边,有 \(r - rpos\)个子数组,最小值在 \(mid\)的右边。
也即 \(lcnt += rpos - mid, rcnt += r - rpos\)
在保证我们始终往范围小的一边枚举端点,通过考察每个端点被枚举的次数可以得到最终算法的时间复杂度为 \(O(n\log n)\)
D. Prime Distance On Tree
题意
给定一棵树,问两点间路径距离为质数的概率
题解
首先预处理出\(5e4\)内的所有质数。(可 \(O(n)\)得到)
较快的暴力
通过枚举路径的两个端点以及预处理每个点到根节点(随便指定一个根)的距离\(dis\),结合lca
可以在\(O(n^2\log n)\)或\(O(n^2)\)的复杂度解决,很显然这是不够的。(假设枚举点\(a,b\),其 lca(最近公共祖先)
是\(c\),那么 \(a \to b\)的距离就是 \(dis[a] + dis[b] - 2dis[c]\)。两点间的lca
可以用倍增等方式在\(O(log n)\)或\(O(1)\)内求出)
点分治+FFT
考虑一个以\(u\)为根的子树,我们通过 \(dfs\)得到一个深度计数数组 \(deep\),\(deep[i]\)表示深度为\(i\)的节点数量(根节点的深度为 \(0\)),我们如果要求长度为 \(len\)的路径条数,其条数为 \(\sum_{i=0}^{len}\limits deep[i] \times deep[len - i]\)。
注意到这是个卷积形式,我们将 \(deep\)数组视为一个多项式的系数,即 \(deep[i]\)表示多项式 \(G(x)\)的项 \(x^i\)的系数。那么将该多项式平方,即 \(G(x) \times G(x)\),其结果中的 \(x^i\)的系数就是长度为 \(i\)的路径条数。(多项式 \(x_i\)的系数的计算公式就是 \(\sum_{j=0}^{i}\limits a_j \times a_{i-j}\), \(a_j = deep[j]\))
因此我们将该多项式乘法结果算出来,将质数项的系数相加,即为结果。
但注意到该结果存在非法路径和重复路径:
- 非法路径即起点和终点在 \(u\)的同一儿子的子树内(此时不应该经过根节点 \(u\))
- 重复路径即不同起点和终点的路径被算两次
前者可通过该儿子子树的\(deep\)数组算出非法路径条数减去,并递归该子树算出合法的路径,后者可以除以2消除。
即通过\(deep\)数组计算出路径,然后通过容斥方法去掉非法路径,并递归子树算出合法路径。
其中求 \(G(x) \times G(x)\)的结果可以用 \(FFT\)在 \(O(n\log n)\)内算出。
为保证递归计算复杂度最低,每次计算选定的根必须是该树的重心,即最大儿子子树的节点数最小的点(这就是点分治算法)。可以证明该重心的最大子树的节点数\(\leq \frac{n}{2}\),这样每次递归计算的规模都会减半,而每次计算的复杂度是 \(O(n\log n)\)
求出了路径条数后,再除以\(\frac{n(n-1)}{2}\)得到概率
因此总的时间复杂度是\(O(n \log^2 n)\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
const int N = 5e5 + 8;
const int UP = 5e4 + 8;
const double eps = 1e-5;
int pri[N];
bool vis[N];
int cnt;
int n;
LL ans;
vector<int> G[N];
void init()
{
for (int i = 2; i < UP; ++i) {
if (!vis[i]) {
pri[cnt++] = i;
}
for (int j = 0; j < cnt; ++j) {
if (1ll * i * pri[j] >= UP)
break;
vis[i * pri[j]] = 1;
if (i % pri[j] == 0) {
break;
}
}
}
}
typedef double LD;
const LD PI = acos(-1);
struct C {
LD r, i;
C(LD r = 0, LD i = 0) : r(r), i(i) {}
};
C operator + (const C &a, const C &b)
{
return C(a.r + b.r, a.i + b.i);
}
C operator - (const C &a, const C &b)
{
return C(a.r - b.r, a.i - b.i);
}
C operator*(const C &a, const C &b)
{
return C(a.r * b.r - a.i * b.i, a.r * b.i + a.i * b.r);
}
void FFT(C x[], int n, int p)
{
for (int i = 0, t = 0; i < n; ++i) {
if (i > t)
swap(x[i], x[t]);
for (int j = n >> 1; (t ^= j) < j; j >>= 1)
;
}
for (int h = 2; h <= n; h <<= 1) {
C wn(cos(p * 2 * PI / h), sin(p * 2 * PI / h));
for (int i = 0; i < n; i += h) {
C w(1, 0), u;
for (int j = i, k = h >> 1; j < i + k; ++j) {
u = x[j + k] * w;
x[j + k] = x[j] - u;
x[j] = x[j] + u;
w = w * wn;
}
}
}
if (p == -1)
FOR(i, 0, n)
x[i].r /= n;
}
void conv(C a[], int n)
{
FFT(a, n, 1);
FOR(i, 0, n)
a[i] = a[i] * a[i];
FFT(a, n, -1);
}
int get_rt(int u)
{
static int q[N], fa[N], sz[N], mx[N];
int p = 0, cur = -1;
q[p++] = u;
fa[u] = -1;
while (++cur < p) {
u = q[cur];
mx[u] = 0;
sz[u] = 1;
for (int &v : G[u])
if (!vis[v] && v != fa[u])
fa[q[p++] = v] = u;
}
FORD(i, p - 1, -1)
{
u = q[i];
mx[u] = max(mx[u], p - sz[u]);
if (mx[u] * 2 <= p)
return u;
sz[fa[u]] += sz[u];
mx[fa[u]] = max(mx[fa[u]], sz[u]);
}
assert(0);
}
int dep[N];
C cnt1[N * 2];
int maxd;
void get_dep(int u, int fa, int d)
{
dep[u] = d;
maxd = max(maxd, d);
cnt1[d].r ++;
for (int &v : G[u]) {
if (vis[v] || v == fa)
continue;
get_dep(v, u, d + 1);
}
}
int count(int u, int len = 0){
maxd = 0;
get_dep(u, -1, len);
LL tmp = -(LL)(cnt1[1].r + eps);
int n2 = 1;
while (n2 <= maxd * 2)
n2 <<= 1;
conv(cnt1, n2);
for (int i = 0; i < cnt; ++i) {
tmp += (LL)(cnt1[pri[i]].r + eps);
}
for (int i = 0; i <= n2; ++i)
cnt1[i].r = cnt1[i].i = 0;
tmp >>= 1;
return tmp;
}
void dfs(int u)
{
u = get_rt(u);
vis[u] = true;
ans += count(u);
for (int &v : G[u]) {
if (vis[v])
continue;
ans -= count(v, 1);
dfs(v);
}
}
int main(void)
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
init();
memset(vis, 0, sizeof(vis));
cin >> n;
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1);
double qwq = (2.0 * ans) / (1ll * n * (n - 1));
cout << fixed << setprecision(10) << qwq << '\n';
return 0;
}