2022黑龙江省赛题解(The 17th Heilongjiang Provincial Collegiate Programming Contest)
A - Bookshelf Filling
题意:
有a,b两种高度的书,a < b,a有n本,b有m本,全部竖着摆放在高度为h的书架上,现在要把至多(m - 1)本b书横着摆放在一些书的顶部。
问这样摆放之后的最小宽度是多少。
题解:
最简单的方法就是二分。这道题显然具有单调性,因为假设一个答案x可行,我们在中间抽出一本书放在右边,答案为x+1就可行。
check很简单,直接判断左边能放多少本就行了。
查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
int a, b, n, m, h;
int check(int x) {
int la = n, lb = x - n;
int cnt = n + m - x;
int cap = 0;
cap += (b - a) * (n / b);
if(cap >= cnt) return 1;
cap += (h - b) * (x / b);
return cap >= cnt;
}
void work() {
cin >> a >> b >> n >> m >> h;
int l = n + 1, r = m + n;
while(l <= r) {
int Mid = (l + r) / 2;
if(check(Mid)) r = Mid - 1;
else l = Mid + 1;
}
cout << l << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int Case;
cin >> Case;
while(Case--) work();
return 0;
}
B - Lovely Fish
题意:
给定一个01串,q个询问,每次问区间[l,r]最少添加多少个1,使得区间的任意前缀,后缀都满足1的数量大于0的数量。
题解:
其实这道题并没有多少跳跃,按照题意去推就有了。
处理前缀的时候我们把1变成1,0变成-1。
首先,我们考虑前缀,记sum为前缀和:
由于我们需要照顾后缀,我们每个1放在越后面越好。所以只有$sum_i==-1$的时候,我们会在这个位置之前放一个1,而放完1之后,所有在他后面的sum都会+1,其余不变。
我们在这一步中,放置的1的个数为$max\{-sum[i]\}(i\in[0, n])$,这个手动模拟一下sum的变化就知道了。
然后处理完前缀之后,我们要处理后缀全为正的问题了。
后缀我们应该写成$sum[n] - sum[i]$。
处理后缀的时候,由于前缀已经全部调整成合法了,所以我们把所有值都堆在$n$位置之后就行了。
那么在这一步中,我们要加的值是:$max\{-(sum'[n]-sum'[i])\}(i\in[0, n])$。
这个值可以写成$max\{sum'[i]\}-sum'[n]$
$sum'$是经过第一步转化后的数组。$sum'[i]$的值是:$sum[i] - min\{sum[j]\}(j\in[0, i])$
由于
$$max\{sum'[i]\}=max\{sum[i]-min\{sum[j]\}\}=max\{sum[i]-sum[j]\}$$
$$sum'[n]=sum[n]-min\{sum[i]\}(i\in[0,n])$$
所以这一步的花费是
$$max\{sum[i]-sum[j]\}-sum[n]+min\{sum[i]\}$$。
联合上一步,总消耗是
$$max\{sum[i]-sum[j]\}-sum[n]+min\{sum[i]\}-min\{sum[i]\}=max\{sum[i]-sum[j]\}-sum[n]$$
所以我们只需要统计区间内$max\{sum[i]-sum[j]\}(i<j,i,j\in[0,n])$的值是多少就行了。题解中的并查集没学会,但是线段树很好写这个东西。
由于0不好处理,因为0相对区间[l,r]代表的是sum[l-1]的值。
所以我们把它拆成
$$max\{max\{sum[i]\} - sum[l - 1], sum[i]-sum[j](i<j, i,j\in[l, r])\}$$
线段树维护这个就很简单了,$ans[rt] = max(ans[lson], ans[rson], max[rson] - min[lson])$。
维护区间答案,以及最大最小值就行了。
然后对应的$sum[n] = sum[r] - sum[l - 1]$,这个随便算一下就行了。
查看代码
//
// Created by onglu on 2022/7/14.
//
#include <bits/stdc++.h>
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define endl '\n'
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define Mid ((l + r) / 2)
//#define int long long
using namespace std;
const int N = 1e6 + 1009;
//const int N = 2e5 + 1009;
//const int N = 5009;
//const int N = 309;
int n, q, sum[N];
struct node {
int val, maxn, minn;
} tree[N * 4];
node operator+(const node &a, const node &b) {
node tmp;
tmp.val = max(a.val, b.val);
tmp.val = max(tmp.val, b.maxn - a.minn);
tmp.maxn = max(a.maxn, b.maxn);
tmp.minn = min(a.minn, b.minn);
return tmp;
}
char c[N];
void update(int rt) {
tree[rt] = tree[lson] + tree[rson];
}
void build(int l, int r, int rt) {
if(l == r) {
tree[rt].val = 1;
tree[rt].maxn= sum[l];
tree[rt].minn = sum[l];
return ;
}
build(l, Mid, lson);
build(Mid + 1, r, rson);
update(rt);
}
node query(int l, int r, int L, int R, int rt) {
if(L <= l && r <= R) return tree[rt];
if(R <= Mid) return query(l, Mid, L, R, lson);
else if(Mid < L) return query(Mid + 1, r, L, R, rson);
else return query(l, Mid, L, R, lson) + query(Mid + 1, r, L, R, rson);
}
int query(int l, int r){
node t = query(1, n, l, r, 1);
return max({t.val, t.maxn - sum[l - 1], 1});
}
void work() {
cin >> n >> q;
cin >> c + 1;
for(int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + (c[i] == '1' ? 1 : -1);
}
build(1, n, 1);
int ans = 0;
for(int i = 1; i <= q; i++) {
int l, r;
cin >> l >> r;
ans ^= (query(l, r) - (sum[r] - sum[l - 1]));
}
cout << ans << endl;
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.in", "r", stdin);
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int Case = 1;
// cin >> Case;
while(Case--) work();
return 0;
}
C - Tree Division
题意:
问能否将树上每个点标上A,B的编号(黑白染色),同时满足:对于任意从根节点出发的路径,A点形成了严格递减序列,B点形成了严格递增序列。
题解:
一开始没想到树形dp,以为有啥构造方法。确定是树形dp之后就很简单了。
我们考虑子树方案,显然根节点会被染上一种颜色,假设是A。
我们发现,根节点一定是子树内最小的元素。而如果对于根节点在原树的父亲节点,假如它染成A颜色,只需要考虑他是不是比根节点小,如果染成B颜色,则需要考虑子树内最大的B颜色是多大,所以我们只需要考虑子树内最大的被染成B的元素是多大就行了。
最直观的方法是$f[i][j]$表示以i为根的子树,最大B元素是j,是否实现。这个显然浪费了很多空间和时间,我们发现只需要关注最大的一个B元素最小能达到多少就行了。
我们修正状态$f[i][0]$表示以i为根的子树,根节点被染成A颜色,时,子树内最大的B元素最小是多少。
同理,$f[i][1]$就是以i为根的子树,根节点被染成B颜色,时,子树内最小的A元素最大是多少。
只考虑$f[i][0]$,我们考虑它的每一棵子树进行转移。
如果子树的根节点大于当前节点,说明可以从$f[j][0]$进行转移;
如果$f[j][1]$大于当前节点,那么可以从子树树根的值进行转移。
两者先取最小,再跟根节点取最大值,就完成转移了。
查看代码
//
// Created by onglu on 2022/7/14.
//
#include <bits/stdc++.h>
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define endl '\n'
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define Mid ((l + r) / 2)
//#define int long long
using namespace std;
const int N = 2e6 + 1009;
//const int N = 2e5 + 1009;
//const int N = 5009;
//const int N = 309;
int n, m, a[N], f[N][2];
vector<int> ver[N];
void dfs(int x, int pre) {
f[x][0] = -1;
f[x][1] = n + 1;
for(auto y : ver[x]) if(pre != y) {
dfs(y, x);
int chose = n + 1;
if(a[y] > a[x]) {
chose = min(chose, f[y][0]);
}
if(f[y][1] > a[x]) {
chose = min(chose, a[y]);
}
f[x][0] = max(f[x][0], chose);
chose = -1;
if(a[y] < a[x]) {
chose = max(chose, f[y][1]);
}
if(f[y][0] < a[x]) {
chose = max(chose, a[y]);
}
f[x][1] = min(f[x][1], chose);
}
}
void work() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
ver[x].push_back(y);
ver[y].push_back(x);
}
dfs(1, 1);
if(f[1][0] != n + 1 || f[1][1] != -1) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.in", "r", stdin);
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int Case = 1;
// cin >> Case;
while(Case--) work();
return 0;
}
D - Collision Detector
题意:
给定平面中三个半径为1的小球,问能否让小球1先与小球2碰撞,然后小球2再与小球3碰撞?
题解:
首先,发现假如小球1与小球3先碰撞了,那么不可能再通过撞击小球2,使得2跟3碰撞。
然后发现,我们可以不考虑小球3,直接先算出小球2撞击后的方向,然后看看小球3是否在上面就行了。
我们可以以小球2为圆心,然后发现小球1与小球2碰撞后,小球2角度是在第一象限和第四象限各$\alpha$角度内飞行。
我们可以算出这个角度,然后再通过计算飞行角度与小球三圆心位置的夹角,可以计算出小球3圆心与x轴的最大夹角(我们可以钦定小球3在第一象限)。
最后判断这个最大角度能否覆盖小球3就行了。
计算后发现就是比较
$$2(\sqrt{BC^2-4}-\sqrt{AC^2-4}) < \overrightarrow{AB} \cdot \overrightarrow{BC}$$
是否成立就行了,因为这里计算的$[0,\pi]$内的cos值,cos值越大,角度越大。
查看代码
//
// Created by onglu on 2022/7/15.
//
#include <bits/stdc++.h>
#define int long long
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define endl '\n'
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define Mid ((l + r) / 2)
//#define int long long
using namespace std;
const int N = 2e6 + 1009;
//const int N = 2e5 + 1009;
//const int N = 5009;
//const int N = 309;
const double eps = 1e-10;
struct Point {
int x, y;
};
Point operator+(const Point &a, const Point &b) {return {a.x + b.x, a.y + b.y};}
Point operator-(const Point &a, const Point &b) {return {a.x - b.x, a.y - b.y};}
istream &operator>>(istream &a, Point &p) {
a >> p.x >> p.y;
return a;
}
int dot(const Point &a, const Point &b) {
return a.x * b.x + a.y * b.y;
}
double len(Point a) {
return sqrt(dot(a, a));
}
Point A, B, C;
void work() {
cin >> A >> B >> C;
Point d = A - B;
d.x /= 2;
d.y /= 2;
if(2 * (sqrt(dot(B - C, B - C) - 4) - sqrt(dot(B - A, B - A) - 4)) < dot(B - A, C - B) - eps) {
cout << "yes" << endl;
} else {
cout << "no" << endl;
}
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.in", "r", stdin);
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int Case = 1;
cin >> Case;
while(Case--) work();
return 0;
}
E - Exclusive Multiplication
题意:
定义
$$f(x) = \prod p_i ^ {a_i \mod 2}$$
其中$p_i$是$x$的质因子,$a_i$是$p_i$唯一分解后的指数。
现在给你一个序列$\{b_i\}$,求
$$\sum_{i = 1}^{n}\sum_{j = i + 1}^{n} f(b_i\times b_j)$$
题解:
挺套路的一个莫反题。
首先我们可以求出$f(a\times b) = f(\frac{a}{gcd(a, b)})\times f(\frac{b}{gcd(,a b)})$。
原因是,重复质数乘两倍,模2之后变成0,就不影响,去掉gcd之后,两个数字就互质了。显然f是一个积性函数 ,所以这样是对的。
我们先去掉$i,j$的顺序关系,得到原式等于:
$$(\sum_{i = 1}^{n}\sum_{j = 1} ^{n} f(b_i\times b_j) - \sum_{i = 1} ^{n} f(b_i^2)) / 2$$
显然$f(x^2) = 1$,所以后面那部分是$n$,我们只需要求下面这个式子
$$\sum_{i = 1}^{n}\sum_{j = 1} ^{n} f(b_i\times b_j)$$
我们通过枚举$b_i,b_j$的值,使得这个式子与序列无关。
$$\Rightarrow \sum_{p = 1}^{N}\sum_{q = 1} ^{N} f(p\times q) \times cnt(p) \times cnt(q)$$
$$\Rightarrow \sum_{p = 1}^{N}\sum_{q = 1} ^{N} f(\frac{p}{gcd(p, q)})\times f(\frac{q}{gcd(p, q)}) \times cnt(p) \times cnt(q)$$
然后我们通过枚举最大公约数,除掉这个gcd:
$$\Rightarrow \sum_{d=1}^{N}\sum_{p = 1}^{\lfloor \frac{N}{d}\rfloor}\sum_{q = 1} ^{\lfloor \frac{N}{d}\rfloor} f(p)\times f(q) \times cnt(pd) \times cnt(qd) \times [gcd(p, q) == 1]$$
后面的布尔表达式用莫比乌斯变换代替,得到
$$\Rightarrow \sum_{d=1}^{N}\sum_{p = 1}^{\lfloor \frac{N}{d}\rfloor}\sum_{q = 1} ^{\lfloor \frac{N}{d}\rfloor} f(p)\times f(q) \times cnt(pd) \times cnt(qd) \sum_{k|p,k|q}\mu(k)$$
我们可以发现p跟q的枚举是类似的,我们通过将$\mu(k)$提前,使得两个凑成平方。
$$\Rightarrow \sum_{k=1}^{N}\mu(k)\sum_{d=1}^{\lfloor \frac{N}{k}\rfloor}\sum_{p = 1}^{\lfloor \frac{N}{dk}\rfloor}\sum_{q = 1} ^{\lfloor \frac{N}{dk}\rfloor} f(pk)\times f(qk) \times cnt(pdk) \times cnt(qdk) $$
$$\Rightarrow \sum_{k=1}^{N}\mu(k)\sum_{d=1}^{\lfloor \frac{N}{k}\rfloor} (\sum_{p = 1}^{\lfloor \frac{N}{dk}\rfloor}f(pk) \times cnt(pdk))^2 $$
这一步的时间复杂度是多少呢?
可以如下计算:
我们枚举$k\times d = t$,原式变成:
$$ \sum_{t=1}^{N}\sum_{k|t} (\mu(k) \sum_{p = 1}^{\lfloor \frac{N}{t}\rfloor}f(pk) \times cnt(pt))^2$$
这个式子的项数显然是跟原式一样的,我们这个式子的项数是$\sum_{i = 1}^{N} d(i) \lfloor \frac{N}{i} \rfloor$
算一下这个复杂度,能通过$2\times 10^5$,那么直接算原式就行了。
还有一个问题是$f$函数能否快速计算。在线性筛中就可以求这个函数了,记录每个点最小质因子出现次数。
当线性筛break前,当前枚举的质数一定是最小质因子,判断它之前的那个质因子最小质因子出现奇数次还是偶数次,再转移。
查看代码
//
// Created by onglu on 2022/7/15.
//
#include <bits/stdc++.h>
#define int long long
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define endl '\n'
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define Mid ((l + r) / 2)
//#define int long long
using namespace std;
const int N = 2e5 + 109;
const int mod = 1e9 + 7;
//const int N = 2e5 + 1009;
//const int N = 5009;
//const int N = 309;
int n, m, a[N + 1], cnt[N + 1];
int mu[N + 1], f[N + 1], g[N + 1], minpricnt[N + 1];
vector<int> pri;
int Pow(int a, int p) {
int ans = 1;
for(; p; p >>= 1, a = a * a % mod)
if(p & 1)
ans = ans * a % mod;
return ans % mod;
}
void work() {
mu[1] = 1;
g[1] = 1;
for(int i = 2; i <= N; i++) {
if(!f[i]) {
pri.push_back(i);
mu[i] = -1;
g[i] = i;
minpricnt[i] = 1;
}
for(int j = 0; j < pri.size() && pri[j] * i <= N; j++) {
f[i * pri[j]] = 1;
if(i % pri[j] == 0) {
mu[i * pri[j]] = 0;
minpricnt[i * pri[j]] = minpricnt[i] + 1;
if(minpricnt[i * pri[j]] & 1) g[i * pri[j]] = g[i] * pri[j];
else g[i * pri[j]] = g[i] / pri[j];
break;
}
mu[i * pri[j]] = -mu[i];
minpricnt[i * pri[j]] = 1;
g[i * pri[j]] = g[i] * pri[j];
}
}
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
cnt[a[i]] += 1;
}
int ss = 0;
for(int k = 1; k <= N; k++) {
for(int d = 1; d <= N / k; d++) {
int sum = 0;
for(int p = 1; p <= N / k / d; p++) {
sum = (sum + g[p * k] * cnt[p * k * d] % mod) % mod;
}
ss = (ss + mu[k] * sum % mod * sum % mod) % mod;
}
}
cout << ((ss - n + mod) % mod * Pow(2, mod - 2) % mod) << endl;
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.in", "r", stdin);
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
int Case = 1;
// cin >> Case;
while(Case--) work();
return 0;
}
F - 342 and Xiangqi
题意:
有两个没有区别的象在棋盘上,问走到对应的位置需要几步。
题解:
什么最短路,什么考虑象按顺序移动不影响,反正没几步就暴力bfs呗,懒得思考了。
查看代码
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
vector<vector<int> > v;
int n, m, s, t;
map<pair<int, int>, int> M;
void work() {
cin >> n >> m >> s >> t;
if(s > t) swap(s, t);
if(n > m) swap(n, m);
M.clear();
queue<pair<int, int> > q;
q.push({n, m});
M[{n, m}] = 0;
while(q.size()) {
int x = q.front().first;
int y = q.front().second;
int d = M[{x, y}];
if(x == s && y == t) {
cout << d << endl;
return ;
}
q.pop();
for(auto z : v[x]) if(z != y) {
int tx = y, ty = z;
if(tx > ty) swap(tx, ty);
if(M.count({tx, ty})) continue;
M[{tx, ty}] = d + 1;
q.push({tx, ty});
}
for(auto z : v[y]) if(z != x) {
int tx = x, ty = z;
if(tx > ty) swap(tx, ty);
if(M.count({tx, ty})) continue;
M[{tx, ty}] = d + 1;
q.push({tx, ty});
}
}
}
signed main()
{
v.push_back(vector<int> {});
v.push_back(vector<int> {2, 3});
v.push_back(vector<int> {1, 4});
v.push_back(vector<int> {1, 4});
v.push_back(vector<int> {2, 3, 5, 6});
v.push_back(vector<int> {4, 7});
v.push_back(vector<int> {4, 7});
v.push_back(vector<int> {5, 6});
ios::sync_with_stdio(0);
cin.tie(0);
int Case;
cin >> Case;
while(Case--) work();
return 0;
}
G - Chevonne's Necklace
题意:
有一个环状序列,每个位置有一个值$c_i$,选定i位置的值,会导致它和它之后$(c_i-1)$个元素被删除(贡献是$c_i$),删除后序列会并拢,不会出现空隙。
问最多删除几个贡献,以及方案数。
两个方案不同,当且仅当被选定的位置的集合不同。
题解:
一开始没看到集合不同,想了快有两个小时。。。
这题似乎在lyd的书上见过,反正就是你任意选定总和不超过$n$的一个集合,你一定有办法把它删干净。
证明也很简单:因为总和不超过n,所以一定不存在循环依赖(你可以把每个元素想象成区间,假如存在循环依赖,总和一定大于n,因为每个元素都被覆盖至少一次)。
那么我们从一个一端元素只被覆盖一次的区间开始删除,那么就能删完了。
然后问题变成了选定总和不超过$n$的元素的方案数。裸的背包。
查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
int f[2009], g[2009], c[2009], n;
signed main()
{
ios ::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> c[i];
}
f[0] = 1;
g[0] = 1;
for(int i = 1; i <= n; i++) if(c[i] != 0) {
for(int j = n - c[i]; j >= 0; j--) {
f[j + c[i]] = (f[j + c[i]] + f[j]) % mod;
g[j + c[i]] |= g[j];
}
}
for(int i = n; i >= 0; i--) if(g[i]) {
cout << i << " " << f[i] << endl;
return 0;
}
cout << 0 << endl;
return 0;
}
H - Kanbun
题意:
给定一个序列,包含'(','-',')',然后输出顺序是:
遇到左括号,跳过,直到输出与他匹配的右括号后,再输出它。
遇到其他元素直接跳过。
题解:
直接递归处理就行了。
先预处理出每个左括号对应的右括号,遇到左括号的时候,先递归输出区间内容,然后输出右括号,最后输出左括号。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 1009;
int n, nxt[N];
char c[N];
void dfs(int l, int r) {
if(l > r) return ;
if(c[l] != '(') {
cout << l << " ";
dfs(l + 1, r);
} else {
dfs(l + 1, nxt[l] - 1);
cout << nxt[l] << " ";
cout << l << " ";
dfs(nxt[l] + 1, r);
}
}
signed main()
{
ios ::sync_with_stdio(0);
cin.tie(0);
cin >> n;
cin >> (c + 1);
stack<int> S;
for(int i = 1; i <= n; i++) {
if(c[i] == '(') S.push(i);
else if(c[i] == '-') {
nxt[i] = i;
} else {
nxt[S.top()] = i;
nxt[i] = i;
S.pop();
}
}
dfs(1, n);
return 0;
}
I - Equal Sum Arrays
题意:
给定一个数$n$,问有多少个正整数序列,满足序列所有元素和等于$n$。
题意:
n < 20,直接暴搜呗,手快的人这时候已经打完了。
正解的话,可以发现这就是n个数的隔板法,直接输出$2^{n - 1}$
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, ans = 0;
void dfs(int res) {
if(res == 0) {
ans += 1;
return ;
}
for(int i = 1; i <= res; i++) {
dfs(res - i);
}
}
signed main()
{
cin >> n;
dfs(n);
cout << ans << endl;
return 0;
}
K - Monkey Joe
题意:
给定一棵树,对于每条路径,路径上的每个点都对答案产生$rank_i\times val_i$的贡献。其中$rank_i$表示路径中有多少个小于等于$val_i$的点数。
保证$val_i$互不相同。
题解:
对于rank提供贡献的题目,我们可以把rank拆分到点上。
对于这道题,具体来说,就是我们枚举一个点对$(i, j)$,满足$val_i \le val_j$。那么这个点对对答案提供$val_j \times path_{i,j}$的贡献,$path_{i,j}$表示同时经过$(i,j)$的路径数量。
我们将点分成四类来统计这个值。
- 当前点子树(不包含自身)内比他小的点。
- 当前点自身。
- 当前点祖先比他小的点。
- 既不是祖先,也不是子树内比他小的点。
对于第一种:根节点是$x$,每个点$y$对答案的贡献是$siz[y] \times (n - siz[x] + 1) \times val[x]$。我们可以用树状数组维护$siz[y]$的和。
怎么求子树内小于$val[root]$的$siz$和呢?
有一个技巧:我们用树状数组维护dfs过的点,小于$val$的$siz$和。当进入子树前,我们查询小于$val[x]$的$siz$和,然后搜索子树。等退出子树的时候,再次查询小于$val[x]$的$siz$和,这两个差值,就是子树内小于$val[x]$的$siz$和。
对于第二种:我们就是统计有多少条经过$x$的路径,我们枚举每个经过他的子树(向上的也看成子树)$siz[y]$,答案就是$\frac{\sum siz[y]\times (n - siz[y] - 1)}{2} + n$。这个意思是,先枚举端点不在根上的路径,每个子树内出去,然后到其他的子树,每条路径被枚举两次,要除2,最后加上从根上出发的路径。
这个对答案的贡献是$val[x]\times pathcnt$。
对于第三种,我们在dfs的时候维护当前点到根的链上的$n - siz[x] + 1$大小,剩下的公式与第一种类似。
第四种也差不多:我们先预处理出所有$val[y]\le val[x]$的子树和,然后减去当前点树内$val[y]\le val[x]$的子树和,再减去到根的路径上的这个东西。剩下的就是不在子树内且不是祖先的子树和了。这里我们需要维护链上的$siz[x]$所以还需要开一个树状数组。
一共开3个树状数组和一个前缀和数组,就可以解决本题了。
查看代码
//
// Created by onglu on 2022/7/15.
//
#include <bits/stdc++.h>
#define int long long
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define endl '\n'
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define Mid ((l + r) / 2)
//#define int long long
using namespace std;
const int N = 2e6 + 1009;
const int mod = 1e9 + 7;
//const int N = 2e5 + 1009;
//const int N = 5009;
//const int N = 309;
int n;
void add(int *tree, int x, int val) {
for(; x <= n; x += x & -x)
tree[x] = ((tree[x] + val) % mod + mod) % mod;
}
int query(int *tree, int x) {
int ans = 0;
for(; x; x -= x & -x) {
ans = ((ans + tree[x]) % mod + mod) % mod;
}
return ans;
}
int Pow(int a, int p) {
int ans = 1;
for( ; p ; p >>= 1, a = a * a % mod)
if(p & 1)
ans = ans * a % mod;
return ans;
}
int inv2;
int ans = 0;
int m, a[N];
int siz[N], val[N], total_sub_tree[N], rk[N];
int treesum[N], treepath[N], path_sub_tree[N];
vector<int> ver[N];
void dfs(int x, int pre) {
siz[x] = 1;
for(auto y : ver[x]) if(y != pre) {
dfs(y, x);
siz[x] += siz[y];
}
}
void cal(int x, int pre) {
add(treesum, rk[x], siz[x]);
add(path_sub_tree, rk[x], siz[x]);
int path_through_x = 0;
int tt = -query(treesum, rk[x] - 1);
for(auto y : ver[x]) if(y != pre) {
path_through_x = (path_through_x + siz[y] * (n - siz[y] - 1) % mod) % mod;
add(treepath, rk[x], n - siz[y]);
int nowsum = -query(treesum, rk[x] - 1);
cal(y, x);
nowsum = ((query(treesum, rk[x] - 1) + nowsum) % mod + mod) % mod;
ans = (ans + nowsum * (n - siz[y]) % mod * val[x] % mod);
add(treepath, rk[x], -(n - siz[y]));
}
path_through_x = (path_through_x + (n - siz[x]) * (siz[x] - 1) % mod) % mod;
path_through_x = path_through_x * inv2 % mod;
path_through_x = (path_through_x + n + mod) % mod;
add(path_sub_tree, rk[x], -siz[x]);
tt = ((tt + query(treesum, rk[x] - 1)) % mod + mod) % mod;
ans = (ans + path_through_x * val[x] % mod) % mod;
ans = (ans + siz[x] * query(treepath, rk[x] - 1) % mod * val[x] % mod) % mod;
ans = (ans + (((total_sub_tree[rk[x] - 1] - tt) % mod - query(path_sub_tree, rk[x] - 1)) % mod) % mod * siz[x] % mod * val[x] % mod) % mod;
}
vector<int> b;
void work() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> val[i];
b.push_back(val[i]);
}
std::sort(b.begin(), b.end());
b.resize(std::unique(b.begin(), b.end()) - b.begin());
for(int i = 1; i <= n; i++) {
rk[i] = lower_bound(b.begin(), b.end(), val[i]) - b.begin() + 1;
}
for(int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
ver[x].push_back(y);
ver[y].push_back(x);
}
dfs(1, 1);
for(int i = 1; i <= n; i++) total_sub_tree[rk[i]] = (total_sub_tree[rk[i]] + siz[i]) % mod;
for(int i = 1; i <= n; i++) total_sub_tree[i] = (total_sub_tree[i] + total_sub_tree[i - 1]) % mod;
cal(1, 1);
cout << (ans % mod + mod) % mod << endl;
}
signed main() {
#ifdef LOCAL
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.in", "r", stdin);
freopen("C:\\Users\\onglu\\CLionProjects\\acm\\data.out", "w", stdout);
#endif
inv2 = Pow(2, mod - 2);
ios::sync_with_stdio(false);
cin.tie(0);
int Case = 1;
// cin >> Case;
while(Case--) work();
return 0;
}
L - Let's Swap
题意:
给定一个字符串,我们可以进行以下操作:
将这个字符串$[1,x],[x + 1, n]$分别进行$reverse$。
但是这个$x$只能取$l_1,l_2$,问给一个s,能否变成t。
题解:
玩一下可能更能明白这道题的意思:
假设对$l_1$进行翻转称为A操作,另一个称为$B$操作。
我们发现每个操作的逆操作是自身,并且操作满足结合律。
也就是说ABBA = 没操作。
这样代表,我们的操作只能是ABABABABABABA或者BABABABABAB这样的
假设$A<B$,我们把AB看成一次操作,模拟一下字符串有哪些变化。
我们发现这次操作在原序列不变的基础上,把最前面(B-A)长度的串扔到了字符串末尾。
然后若干次循环以后,这个段又回到最开始,这就类似于更相减损法,所以这个操作的最小移动单元是gcd(n,B-A)。
我们枚举每一个长度为k*(B-A)的前缀移动到末尾之后,是否与t相同就行了。
这时候我们注意,还有一个操作是BABABAB,它是以B开头的,所以我们先进行一次B,然后就变成跟第一个操作一样的处理方法了。
查看代码
#include <bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int N = 5e5 + 1009;
const int mod = 257;
char s[N], t[N], tmp[N];
ull hs[N], pw[N];
int a, b, c, n;
ull ask(int l, int r) {
return hs[l] - hs[r + 1] * pw[r - l + 1];
}
ull get(int pos) {
return ask(pos, n) + ask(1, pos - 1) * pw[n - pos + 1];
}
int check() {
ull tar = 0;
pw[0] = 1;
for(int i = 1; i <= n; i++) pw[i] = pw[i - 1] * mod;
for(int i = n; i >= 1; i--) {
tar = tar * mod + t[i];
}
hs[n + 1] = 0;
for(int i = n; i >= 1; i--) {
hs[i] = hs[i + 1] * mod + s[i];
}
for(int i = 1; i <= n; i += a) {
if(tar == get(i + a)) {
return 1;
}
}
return 0;
}
void work() {
cin >> s + 1 >> t + 1;
cin >> a >> b;
n = strlen(s + 1);
c = a;
a = max(a, b) - min(a, b);
a = __gcd(a, n);
if(check()) {
cout << "yes" << endl;
return ;
}
for(int i = 1; i <= n; i++) tmp[i] = s[i];
int cnt = 0;
for(int i = c; i >= 1; i--) s[++cnt] = tmp[i];
for(int i = n; i > c ; i--) s[++cnt] = tmp[i];
if(check())
cout << "yes" << endl;
else cout << "no" << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
int Case; cin >> Case;
while(Case--) work();
return 0;
}