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)$的路径数量。

我们将点分成四类来统计这个值。

  1. 当前点子树(不包含自身)内比他小的点。
  2. 当前点自身。
  3. 当前点祖先比他小的点。
  4. 既不是祖先,也不是子树内比他小的点。

对于第一种:根节点是$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;
}
posted @ 2022-07-14 10:20  _onglu  阅读(931)  评论(0编辑  收藏  举报