CCF CSP 202112-5 极差路径

题意:给你一棵树,定义一条路径(x,y)被推荐的,当且仅当

\displaystyle \min (x,y) - k_1 \leq \min P(x,y) \leq \max P(x,y) \leq \max (x,y) + k_2

\min P(x,y) 表示从xy的简单路径上的编号最小值,\max P(x,y)同理。求被推荐的路径条数,(x,y)(y,x)被视为同一条路径。

容易想到这种路径计数还带\min \max的无非就三种做法:并查集、树上DP/按秩合并、点分治。前面两种做法局限性比较强,而这里的条件比较多,难以维护,于是我们考虑直接用点分治来做。假如我们目前处理的根节点为u,我们需要统计所有经过u的合法路径。从u开始dfs一遍之后我们得到了从u出发到各个点的路径的\min \max,我们用三元组去表示,记作(v,minv,maxv)。然后合法路径的条数就是满足v_1 < v_2, v_1 - k_1 \leq min(minv_1,minv_2),max(maxv_1,maxv_2) \leq v_2 + k_2的三元组(v_1,minv_1,maxv_1), (v_2,minv_2,maxv_2)的对数。这就是一个三维数点问题了,可以用CDQ在O(n \log^2 n)时间或者用可持久化线段树在O(n \log n)时间复杂度内解决。如果套上点分治的话,CDQ复杂度就有点高了,于是我选择了可持久化线段树。

具体如何使用可持久化线段树实现这个三维数点,方法有很多。我这里是按照点的编号将三元组排序后,按编号从小到大依次处理。v_1 - k_1 \leq min(minv_1,minv_2)可以拆分成v_1 - k_1 \leq minv_1v_1 - k_1 \leq minv_2,前面的式子可以直接判断,后面的式子的处理将会在最后说明。同样的,我们将max(maxv_1,maxv_2) \leq v_2 + k_2拆分成maxv_1 \leq v_2 + k_2maxv_2 \leq v_2 + k_2,后面的式子可以直接判断。

最后我们还剩下两个限制v_1 - k_1 \leq minv_2maxv_1 \leq v_2 + k_2。只需要在root[v_1-k_1]线段树的maxv_1位置进行+1操作。然后枚举到v_2时,对应的以v_2为大端的路径条数就是root[minv_2]中区间1 \cdots v_2+k_2的和。由于v_i是从小到大枚举的,所以可以直接维护。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#pragma GCC optimize(3)
using namespace std;
const int maxn = 5e5+10;
const int INF = 1e9 + 10;
vector<int> g[maxn];
int S, Mx, K1, K2, n, root;
int sm[maxn], mxson[maxn], vis[maxn];
char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<23],*O=obuf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
inline int rd() {
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
    return x*f;
}
void getrt(int u, int fa){
    sm[u] = 1;
    mxson[u] = 0;
    for (int v : g[u]) if (!vis[v] && v != fa){
        getrt(v, u);
        sm[u] += sm[v];
        mxson[u] = max(mxson[u], sm[v]);
    }
    mxson[u] = max(mxson[u], S - sm[u]);
    if (mxson[u] < Mx){
        root = u;
        Mx = mxson[u];
    }
}
void get(int u, int fa, vector<int> & nodes, pair<int, int> *value, int mn, int mx) {
    nodes.push_back(u);
    value[u].first = mn;
    value[u].second = mx;
    for (int v : g[u]) if (!vis[v] && v != fa){
        get(v, u, nodes, value, min(mn, v), max(mx, v));
    }
}
int rt[maxn], sz[maxn * 20], ch[maxn * 20][2], top = 0;
int newnode(int x){
    sz[++top] = sz[x];
    ch[top][0] = ch[x][0];
    ch[top][1] = ch[x][1];
    return top;
}
void ins(int &rt, int l, int r, int val) {
    rt = newnode(rt);
    sz[rt]++;
    int t = rt; 
    while (l < r){
        int mid = l + r >> 1;
        if (val <= mid){
            ch[t][0] = newnode(ch[t][0]), t = ch[t][0], sz[t]++, r = mid;   
        }else{
            ch[t][1] = newnode(ch[t][1]), t = ch[t][1], sz[t]++, l = mid + 1;
        }
    }
}
int get(int rt, int l, int r, int x){
    int cnt = 0;
    while (l < r){
        int mid = l + r >> 1;
        if (x <= mid) rt = ch[rt][0], r = mid;
        else cnt += sz[ch[rt][0]], rt = ch[rt][1], l = mid + 1;
    }
    cnt += sz[rt];
    return cnt;
}
long long solve(int v, int mn, int mx){
    vector<int> nodes;
    static int w[maxn];
    static pair<int, int> value[maxn];
    get(v, 0, nodes, value, min(mn, v), max(mx, v));
    for (int i = 0; i < nodes.size(); i++) w[i] = nodes[i];
    sort(w, w + nodes.size());
    long long cnt = 0;
    top = 0;
    rt[0] = 0;
    for (int i = 0; i < nodes.size(); i++) {
        auto p = value[w[i]];
        if (i) rt[i] = rt[i - 1];
        if (w[i] - K1 <= p.first) ins(rt[i], 1, n, p.second);
        if (w[i] + K2 >= p.second) {
            int nv = p.first + K1;
            //int pos = min(int(upper_bound(w, w + nodes.size(), nv) - w - 1), i);
            int l = -1, r = nodes.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (w[mid] > nv) r = mid - 1; else l = mid; 
            }
            int pos = min(l, i);
            if (pos >= 0) {
                cnt += get(rt[pos], 1, n, w[i] + K2);
            }
        }
    }
    return cnt;
}
long long ans = 0;
void Divide(int rt){
    ans += solve(rt, INF, 0);
    vis[rt] = 1;
    for (int v : g[rt]) if (!vis[v]){
        ans -= solve(v, rt, rt);
        S = sm[v];
        root = 0;
        Mx = INF;
        getrt(v, 0);
        Divide(root);
    }
}
int main(){
    n = rd();
    K1 = rd();
    K2 = rd(); 
    for (int i = 1; i < n; i++){
        int u, v;
        u = rd();
        v = rd();
        g[u].push_back(v);
        g[v].push_back(u);
    }
    Mx = INF;
    S = n;
    getrt(1, 0);
    Divide(root);
    cout << ans << endl;
}

背景:

报名了今年3月份的csp认证,于是尝试模拟了一下上次的csp认证试题。

前面4题没啥好说的,但这题的难度突然上升,思前想后找到了一个点分+三维数点的做法,写完后直接TLE。然后经历了2个小时的卡常过后,从84分卡到了96分,还有4分实在卡不下去了(卡吐了)。

ICPC 2019 NWRRC D. Double Palindrome

求长度小于等于n,由前k个小写字母组成的字符串中,满足双回文串定义的有多少个。

双回文串:本身为回文串或者由两个回文串拼接成的字符串。

先计算双回文串可能的数量,令

\displaystyle R(n) = \sum_{i=0}^{n-1}k^{\lceil \frac{i}{2} \rceil}k^{\lceil \frac{n-i}{2} \rceil} 

考虑减去那些重复计算的字符串。可以发现如果一个字符串p的划分方式不唯一,那么这个字符串必然可以写成s \times m的形式,其中s是一个划分唯一的串。然后通过R(n)的表达式以及s \times m的性质,这个字符串被统计了m次。

则唯一划分字符串个数的表达式为\displaystyle D(n) = R(n)-\sum_{l|n, l < n} \frac{n}{l} D(l)。最后答案为\displaystyle \sum_{i=1}^n \lfloor \frac{n}{i} \rfloor D(i)

R(n)可以通过简单的数学推导在线性时间复杂度内求出,其余计算的时间复杂度O(n \log n)

#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 100010;
int f[maxn];
int pow(int x, int u){
    int y = 1;
    for (; u; u >>= 1, x = 1LL * x * x % mod) if (u & 1) y = 1LL * x * y % mod;
    return y;
}
int main(){
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        if (i & 1) f[i] = 1LL * i * pow(k, (i + 1) / 2) % mod;
        else f[i] = (1LL* i / 2 * pow(k, i / 2) + 1LL * i / 2 * pow(k, i / 2 + 1)) % mod;
    }

    for (int i = 1; i <= n; i++)
        for (int j = i + i; j <= n; j += i)
            f[j] = (f[j] - 1LL * j / i * f[i]) % mod;
    int ans = 0;
    for (int i = 1; i <= n; i++) ans = (ans + 1LL * f[i] * (n / i)) % mod;
    if (ans < 0) ans += mod;
    cout << ans << endl;
}

Educational Codeforces Round 97 F. Emotional Fishermen

给出一个长度为n~(2\leq n \leq 5000)的数组a_1,a_2,a_3,...,a_n~(1\leq a_i \leq 10^9),求有多少种将a重新排列后的b满足对\forall~ 2 \leq i \leq n都有b_i \geq 2max\{b_1,b_2,...,b_{i-1} \}2b_i \leq max\{b_1,b_2,...,b_{i-1} \}

先将数组a升序排序。用f[i][j]表示最大值为a[i]长度为j的序列总数。考虑如下两种转移:

  • 在序列末尾加入一个大于两倍最大值的数,f[k][j+1] \leftarrow f[i][j]a_k \geq 2a_i,实现时使用前缀和优化
  • 从满足2a_k \leq a_i且未在序列中的数a_k中选择一个加入序列末尾,f[i][j+1] \leftarrow f[i][j] * (k - j + 1),其中k表示最大的k满足2a_k \leq a_i

时间复杂度O(n^2)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5010;
const int mod = 998244353;
int a[maxn]; 
int sum[maxn][maxn], f[maxn][maxn];
int main(){
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + 1 + n);
    int t = 0; sum[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        while (2 * a[t + 1] <= a[i]) t++;
        for (int j = 1; j <= i; j++) {
            f[i][j] = sum[t][j - 1];
            if (j - 1 <= t) {
                (f[i][j] += 1ll * f[i][j - 1] * (t - j + 2) % mod ) %= mod;
            }
        }
        for (int j = 0; j <= i; j++)
            sum[i][j] = (sum[i - 1][j] + f[i][j]) % mod;
    }
    cout << f[n][n] << endl;
}

2020CCPC秦皇岛 I. Interstellar Hunter

给出一系列二维向量(x_i,y_i)~(0 \leq x_i,y_i \leq 10^6),问一个给定向量(x,y)是否可以被这些向量的整数组合表示。

首先显然向量组\{\boldsymbol{a}, \boldsymbol{b}\}与向量组\{\boldsymbol{a} \pm \boldsymbol{b}, \boldsymbol{b}\} 以及向量组 \{\boldsymbol{a}, \boldsymbol{b} \pm \boldsymbol{a}\}所能生成的点集等价。

于是若已知向量组\{\boldsymbol{a}=(x_1,y_1), \boldsymbol{b}=(x_2,y_2)\},设g = (y_1, y_2),那么我们可以通过类似辗转相除的方法将向量组变成\{(tx_1, 0), (tx_2, g) \}。其中

\displaystyle tx_1 = \frac{y_2}{g}x_1 - \frac{y_1}{g}x_2, tx_2 = ax_1 + bx_2, ay_1 + by_2 = g

再加入一个新向量(x_3,y_3)时,可以继续把该向量与向量(tx_2,g)变换成(x,0)(tx_3,(g,y_3)),其中向量(x,0)可以和之前的向量(tx_1,0)通过gcd合并。

于是我们可以维护一个大小为2的与输入向量等价的向量基来进行判断。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll exgcd(ll a,ll b,ll &x,ll &y){
    if(b == 0){
        x = 1; y = 0; return a;
    }
    ll ret = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return ret;
}
int main(){
    int t, cas = 0;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        ll x = 0, y = 0, d = 0, ans = 0;
        while(n--){
            ll opt, tx, ty, w;
            scanf("%lld%lld%lld", &opt, &tx, &ty);
            if(opt == 1){
                if(ty == 0){
                    d = __gcd(d, abs(tx));
                    continue;
                }
                if(ty < 0) ty *= -1, tx *= -1;
                ll g, a, b;
                g = exgcd(y, ty, a, b);
                d = __gcd(abs(x * ty / g - tx * y / g), d);
                y = g;
                x = a * x + b * tx;
                if(d) x = (x % d + d) % d;
            }
            else{
                scanf("%lld", &w);
                if(y == 0){
                    if(d == 0){
                        if(tx == 0 && ty == 0)
                            ans += w;
                    }
                    else if(tx % d == 0 && ty == 0)
                        ans += w;
                }
                else if(ty % y == 0){
                    if(d && (tx - ty / y * x) % d == 0)
                        ans += w;
                    else if(!d && !(tx - ty / y * x))
                        ans += w;
                }
            }
        }
        cout << "Case #" << ++cas << ": " << ans << endl;
    }
    return 0;
}

东北赛回顾

1001

中期的时候喵了一下这题,成功把模型转换成最小生成树,然后就不会做了。

1002

网络流裸题。ly写完+WA*2,AC。

1003

题目大意:定义了一个模(x+1)的类似hash的函数F(x),再定义了一个函数g(n, m),表示一个数n经过mF嵌套后得到的值。求g(n, 1)g(n, m)的和。

solution:发现有人过了此题,我就去看了。开始感觉没什么思路,搞不懂这个F(x)是在干啥,也找不到什么规律。思考了一会之后感觉这个%(x+1)必有蹊跷,这样x只增不减,说不定很快就收敛了。然后迅速写了一个暴力发现果然是这样,交了一下WA。原来我的F(x)x=0的时候会输出1。。。修改了一下就过了。成功贡献一发罚时。

1004

题目大意:糖豆人模拟题。一个皇冠在0-H的高度上以1/s的速度上下移动,当且仅当皇冠高度小于等于h时,糖豆人才能够得着。给出每个人到达皇冠的时间和延迟,求最后谁最先摸到皇冠。

solution:本场我过的第二道题(说实话第一次看到此题的时候直接就跳了,以为会花很多时间)。做法很简单,计算到达皇冠的时候皇冠的位置,如果能够得着就直接结束,不行就再模拟计算一下皇冠下一次到达h的时间。最后加上延时sort一下。

1005

题目大意:构造一个包含NN01向量的向量组,每个向量中1的个数为K,且向量组在XOR运算下线性无关。

solution:写完1003就来看这题了,本以为可以通过贪心+线性基求第k大+二分过此题,然后发现事情没有这么简单,挣扎了10分钟后放弃了去看ly题。最后在czq写了一个较为奇妙但又WA又T的暴力然后不经意间看到了输出的答案发现了构造方法后PE了一下才A掉了这题(这合理吗.jpg)。

1007

题目大意:有K个人在打牌。有A,B,G,P四种牌,牌上有一个数字x。现在要给这K个人轮流发一共N张牌,每发一张牌会覆盖那个人上一次得到的牌。记cnt_i为发了这张牌后,桌面上数字和为5的牌的种类数。求cnt_1cnt_N的和。

solution:本场我过的第一题。说实话这题意我看了足足有5min。交的第一发初始化把i打成了另一个变量然后WA了一下。成功贡献一发罚时。做法也很简单,直接模拟。

1008

题目大意:给一个小写字符串,每次可以将一个连续的子串压缩,压缩方式是将连续的字母以单字母紧接一个16进制数来表示个数。现可以从原串中最多删去一个字母(删去后,两端的字符串不连续),求一个压缩结果最短的方案。若有多个方案使压缩结果最短,输出字典序最小的。

solution:看完1009就来看此题,和ly讨论了一下大致做法后就开写。一共WA了四次,原因是没发现可以选择子区间压缩:总结,一道阅读理解题+模拟题。做法就是将连续的字母提取出来,然后分为几种情况考虑。1.只有一个字母。2.连续两个字母。3.连续大于2个。对于第一种情况,不压缩比压缩优,于是删了后会使长度减少1;若删了后字典序变小(与后一个字母比较),则优先考虑位置靠前的否则尽量靠后。对于第二种情况,压缩比不压缩优(a2比aa字典序小),删了后会形成单字母,长度减少1,而字典序一定会增大,则尽量往后考虑。对于第三种情况,又分为两种情况:退位和不退位。退位(1000->FFF)长度-1,字典序增大,尽量往后考虑。不退位(1100->10FF, 1A00->19FF,etc.)长度不变,字典序减小,那么在长度-1不存在的情况下,选择首位删除。

1009

题目大意:给一个n*ndis矩阵,保证所对应的图联通且的边长是定值,求一个边数最小的子图,满足Dis矩阵和原dis矩阵相同。输出边数。

solution:在看1005的时候听ly高呼说这是多校原题()。我想那应该稳了。过了一会ly说复杂度不太对,然后我去看了一下题,看见了边长相等这个条件,此题瞬间变成了签到题(这个条件需要稍微细心一点才能发现,出题人还是需要在题面中强调一下)。ly发现事情真相后破口大骂,随即A掉此题。

1011

本场过的最后一题。当时不知道是否可以连续憋气,加上写了一个倍增维护答案,就一直WA。然后发现K大于1000的时候我的数组会溢出(竟然没有显示RE)。最后输入K的时候对1000取了一个min就A了。感觉挺离谱的。标程是用的单调队列,比倍增少个log,还很好写。当时怎么没想到用单调队列啊。。

1012

czq肝到比赛结束依然没有肝出来。

1013

签到题,czq写完一发A。

总结

这次夺冠也是我万万没有想到的。一些不足之处在于边界情况忘判导致的罚时以及题意阅读上的疏漏。