2020年校赛网络赛

51假期那段时间因为水了一段时间的数模校赛,加上专业课的坑越欠越多…因而已经很久很久没有补过题目了…

从近到远,先把西电校赛的坑填起来,再把之前的CF牛客Atcoder补起来…qaq


C
发现交换律后就显然了。

D
双指针。

好像第一天晚上还有个小兄弟通过实验群问我这个题目,我机智地拒绝了他。
结果第二天就看到他因为代码抄袭被禁赛的消息

E
做的时候一直在用假算法…WA了大概15发才意识到可能真的是个赤裸裸的模版题…

SCC缩点 & 构造超级源点跑dijstra

(标程中没有构造源点直接用的拓扑排序&dp,比我的dijstra少个log的复杂度,不过问题不大)

一直没写出来的原因主要在于…SCC这一块的知识点学的一塌糊涂(遥记是第一天暑训 & 第一天爆零)…而且平时CF的训练基本是没有图论的…之前打的5小时大赛的题目也不知道有没有这种纯模版题,最终导致了模版题切的自闭的一比。。

F
场上打的时候其实没有系统训练过正统的数论分块,然而我自己根据这种思想手撸了一个虽然野但是不假的数论分块qwq

赛后补题真是一言难尽…首先还是先处理一下这道题中涉及到的数论分块问题:('/'号即表示向下取整的除法)

  • 形式 n k \frac{n}{k} kn至多有 2 n 2\sqrt{n} 2n 个不同的结果。这是因为当 k < n k <\sqrt{n} k<n 时,一个k对应一个结果,至多k种; k > n k > \sqrt{n} k>n 时,除式 n k \frac n k kn小于 n \sqrt{n} n ,至多也只有 n \sqrt{n} n 种。
  • 假设数 i i i满足 n i = p \frac ni=p in=p j j j是最大的可以满足 n j = p \frac nj = p jn=p的数,那么j可以表示为 n p \frac np pn也就是 n n i \frac n{\frac ni} inn。容易证明:数 j j j满足等式,则有 p < = n j < p + 1 p <= \frac nj < p + 1 p<=jn<p+1;从而 j < = n p = n / n i j <= \frac np=n/\frac ni j<=pn=n/in.

第一个结论保证了时间复杂度,第二个结论为我们具体求解提供了一种途径。
因而只要顺序枚举每一个数块的左右端点,就可以快速地求解这道题。

然而…事实上是,尽管算法相当简单,但是一些细节坑死人…

  • 因为“乘除模”优先级相同,从左到右进行运算。所以在写连乘/模式时,要注意从左到右乘的所有中间量都不能爆long long. 像res % M * n % M就很有可能会出事情。
  • 取模运算的一些性质一定要仔细推导再谨慎下手…在求[left, right]的所有数之和时,尽管一定有 2 ∣ ( l e f t + r i g h t ) ∗ ( r i g h t − l e f t + 1 ) 2 | (left + right) * (right - left + 1) 2(left+right)(rightleft+1)但是不一定有 2 ∣ ( l e f t + r i g h t ) % M ∗ ( r i g h t − l e f t + 1 ) % M 2 | (left + right) \% M * (right - left + 1) \% M 2(left+right)%M(rightleft+1)%M
    这里直接想当然地认为可以,才导致了最终WA死看瞎也找不到错误qwq
    (我最开始在干什么…这怎么可能可以成立…)

最后贴一下代码吧…

ll sum(ll left, ll right){
    ll a = left + right;
    ll b = right - left + 1;
    if(a % 2 == 0) return temp = ((a / 2) % M * (b % M)) % M;
    else return temp = ((b / 2) % M * (a % M)) % M;
}

int main(){
//    Fast;
//    freopen("out.txt", "w", stdout);
    scanf("%lld %lld", &n, &M);
    ll ans = 0, res1, res2;
    ll left = 1, right;
    while(left <= n){
        res1 = 0; res2 = 0;
        ll p = n / left; right = n / p; p %= M;

        res1 = (right - left + 1) % M * p % M; res1 *= n % M; res1 %= M;
        
        res2 = (res2 + sum(left, right) * p % M * p % M) % M;
        ans += res1 % M - res2 % M; ans = (ans % M + M) % M;
        
        left = right + 1;
    }
    cout << ans << '\n';
    return 0;
}

G
也是一道模版然而没有看出来的题目,不过学到了不少。

Manacher & 二阶差分

尽管之前为了防止CF上出一些披着回文串外衣实质上是思维 | dp的题而专门去学习了一下Manacher算法,但是隐约记得时间复杂度好像不是很优…最坏的情况可能还是得一步步地遍历…事实上我的记忆确实很“隐约”,马拉车的时间复杂度是 O ( n ) O(n) O(n),用来得到以任意点为中心的回文串的最长长度…这正是这道题所需要的。

自己想的时候正反两面都考虑了一番,发现这道题正反两面要求的东西是一致的,所以最终还是回归到了求回文串的问题上来。我的朴素算法是一个很暴力的暴力…但是我居然没看出来…暴力的依据在于:对任意一个以a[i]为终点的回文串,一定可以由以a[i - 1]为终点的某一个回文串构造出来,并且除了他本身构成的自回文串不可能存在更短而无法用a[i - 1]为终点的回文串构造出来的的回文串。那么就直接搜呗…太坏了,复杂度是 O ( n 2 / 26 ) O(n ^ 2 /26) O(n2/26) (不过用来对拍还是8错的)

朴素TLE的对拍算法

const int maxn = 1e6 + 10;
 
int n, m;
char temp[maxn];
 
 
struct NODE{
    int key, w;
}node[maxn];
int top = 0, id[maxn];;

//ans是结果数组,res是差分数组;最终的结果是ans + sum[res]
ll ans[maxn], res[maxn];
//id[i]表示第i组元素的开始位置。
unordered_set<int> s;
//del用来记录一次操作后需要-1的点,而在加入的时候如果为0,那么不加入。
vector<int> del, ins;
void sove(){
    s.clear();
     
    int d;
    for(int i = 0; i < top; i++){
        d = node[i].w;
        //内回文的情况;
        //只有这里会设计到int * int > int,后面的都是用的差分数组
        for(int pos = id[i]; pos < id[i] + d; pos++){
            ans[pos] += (ll)(pos - id[i] + 1) * (ll)(d - (pos - id[i]));
        }
        //pre是前一组的元素为终点、所能构成的回文串的所有起点的“组”标号;
        //即完整的一组node[i - 1]和pre中的部分后缀可能构成一个回文串; 那么显然,必须满足pre.key == node[i - 1].key并且pre.w > node[i-1].w
        for(auto pre: s){
            if(pre == 0) del.push_back(0);
            else{
                if(node[i - 1].w == node[pre].w){
                    //长度相同并且之前一个元素也是当前元素
                    if(node[pre - 1].key == node[i].key){
                        //如果之前的长度甚至比现在更长,那么当前一段全部可以成为右端点。
                        //这两种情况都可以更新答案。
                        if(node[i].w <= node[pre - 1].w){
                            for(int dis = 0; dis < node[i].w; dis++){
                                //设置差分数组。
                                res[id[pre] - 1 - dis]++; res[id[i] + dis + 1]--;
                            }
                            del.push_back(pre); ins.push_back(pre - 1);
                        }
                        else{
                            for(int dis = 0; dis < node[pre - 1].w; dis++){
                                res[id[pre] - 1 - dis]++; res[id[i] + dis + 1]--;
                            }
                            //因为不可能成为重点,所以索性不加入set中
                        }
                    }
                    //尽管长度相同但是中之前一个元素不是当前元素
                    else del.push_back(pre);
                }
                //长度都不相同,那么已经构不成回文串了
                else del.push_back(pre);
            }
        }
        for(auto x: del) s.erase(s.find(x));
        for(auto x: ins) if(x != 0) s.insert(x); s.insert(i);
        del.clear(); ins.clear();
    }
}
 
int main(){
    Fast;
    freopen("ans.txt", "w", stdout);
//    time_t start = clock();
//    freopen("out.txt", "w", stdout);
    scanf("%d %d", &n, &m);
    for(int i = 0; i < m; i++){
        scanf("%s", temp);
        itn p = 0; top = 0; while(p < n){
            id[top] = p;
            node[top].key = temp[p] - 'a'; node[top].w = 1;
            while(p + 1 < n && temp[p + 1] == temp[p]){
                p++;
                node[top].w++;
            }
            p++; top++;
        }
        sove();
    }
    ans[0] += res[0];
    for(int i = 1; i < n; i++){
        res[i] += res[i - 1];
        ans[i] += res[i];
    }
    for(int i = 0; i < n; i++){
        printf("%lld", ans[i]);
        printf(i != n - 1? " ": "\n");
    }
     
//    time_t end = clock();
//    printf("%.3f\n", (end - start) * 1.0 / CLOCKS_PER_SEC);
    return 0;
}

但是如果有了Manacher预处理出来的半径长度数组p,就可以比较容易的解决这个问题。因为对于一些较小的回文串,如果它被包含在了一个大回文串内,那么可以直接通过大回文串的相关参数来确定这一段区间总共的加分情况。

以任意一个位置为中心的最大回文串长度可以通过Manacher超快的做出来,而该区间的具体加分如下所述:
设起点为 x x x,回文串长度为 d d d。当 d d d为奇数时,容易知道区间[x, x + d - 1]的分值变化情况为[1, 2, ..., k, k + 1, k, k - 1, ..., 2, 1],d为偶数时,变化情况为[1, 2, ... ,k, k, k - 1, ... ,2 , 1]。之前我基本只见过区间的一阶差分,表现形式为区间加减;而实际上差分可以用高阶等差数列的思想去考虑,因而像这种一阶等差数列可以通过两次差分令其常见的容易修改的差分数列,具体规律可以手动列举模拟一番。

而对于如何差分的办法,通常采用前向差分,即 Δ f ( k ) = f ( k + 1 ) − f ( k ) Δf(k) = f(k + 1) - f(k) Δf(k)=f(k+1)f(k),这样下标会不断前缩,容易计数;如果采用后向差分,下标会不断后缩,不是很方便。

最后注意一下longlong,就可以AC这道题。


const int maxn = 1e6 + 10;

/*
 Manacher算法
 读入一个字符串temp及其长度n, 构造出s并赋予截断标志, 自动初始化、获得数组p[i];
 对于某个位置i∈[0, 2 * n], 以i为中心的回文串的参数如下:
    int start = (i / 2) - (p[i] / 2) + (i & 1), d = p[i] - 1;
 */

int n;
char temp[maxn], s[maxn << 1];
int p[maxn << 1];
void manacher(){
    scanf("%s", temp);
    for(int i = 0; i < n; i++){
        s[i << 1] = '#'; s[i << 1 | 1] = temp[i];
    }
    s[n << 1] = '#'; s[n << 1 | 1] = 0;
    
    int id = 0, right = 0;
    for(int i = 0; i <= n << 1; i++) p[i] = 1;
    for(int i = 1; i <= n << 1; i++){
        if(i <= right) p[i] = min(right - i + 1, p[2 * id - i]);
        while(i + p[i] <= 2 * n && i - p[i] >= 0 && s[i + p[i]] == s[i - p[i]])
            p[i]++;
        if(right < i + p[i] - 1){
            id = i; right = i + p[i] - 1;
        }
    }
}

ll ans[maxn];

void sove(int x, int d){
    if(d & 1){
        ans[x] += 1; ans[x + d / 2 + 1] -= 2; ans[x + d + 1] += 1;
    }
    else{
        ans[x] += 1; ans[x + d/2] -= 1; ans[x + d/2 + 1] -=1; ans[x + d + 1] += 1;
    }
}

int main(){
//    Fast;
//    freopen("out.txt", "w", stdout);
    int t; scanf("%d%d", &n, &t);
    while(t--){
        manacher();
        for(int i = 0; i < n << 1; i++){
            int start = (i / 2) - (p[i] / 2) + (i & 1), d = p[i] - 1;
            if(d){
                sove(start, d);
            }
        }
    }
    for(int i = 1; i < n; i++) ans[i] += ans[i - 1];
    for(int i = 1; i < n; i++) ans[i] += ans[i - 1];
    for(int i = 0; i < n; i++){
        printf("%lld", ans[i]);
        if(i != n - 1) printf(" "); else printf("\n");
    }
    return 0;
}

之后打算整理一下自己的板子…因为尽管知道之前某场CF学了Manacher但是这么多博客我肯定找不到qaq…

H
随便找一组呈60°角的单位向量作为坐标系的基向量,然后根据六边形构造的规律性快速求出两个点的坐标,最后根据象限的不同进行适当的60°/120°旋转操作后就可以直接根据坐标情况求到最短路径长度和方案数。

J
DP & 组合计数
这两个都不能算是不会的知识点…确切说起来应该算是薄弱的知识点qwq

感觉自己处理这类题目的时候没有什么思路…只能从一种纯数学的角度进行分析求解,然而这样的思路在ACM中往往是比较狭隘片面的。

学习于题解。
题面约束两个条件: 1.每个数字都要出现一次 2. 任意长度为k的区间都不能是1-k的一个排列,也就是必须要有两个重复的数字出现。

我们先满足条件2,再通过其他的一些计数办法来设法求解满足另一个约束条件的种数。为此,我们设dp数组dp[i][j]表示从i开始往前走至多只有j个数字两两不同。进而容易获得状态转移式: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] ∗ ( k − j + 1 ) + ∑ x = j k − 1 d p [ i − 1 ] [ x ] dp[i][j] = dp[i - 1][j - 1] * (k - j + 1) + \sum_{x = j}^{k - 1}dp[i - 1][x] dp[i][j]=dp[i1][j1](kj+1)+x=jk1dp[i1][x]
左边一项可以理解为残缺 + 添项构造,右边一项可以理解为盈余 + 截断。另外,因为必须满足条件2,所以j最长不能取到k!后面的状态只能由前面那些满足条件2的状态转移得来。

接下来我们求出其中还满足条件1的构造种数。由dp数组,我们容易求得使用至多k个数字构造出满足条件2的乐谱的种数,此外,我们还很容易知道,如果使用至多x < k个数字构造乐谱,那么一定可以满足条件2,种数也易求得: f x = ( x k ) ∗ x n f_x=(^k_x)*x^n fx=(xk)xn

那么为了要求恰使用k种数字,利用容斥原理即可。

a n s = ∑ i = 1 k ( − 1 ) k − i ( i k ) f i ans = \sum_{i= 1}^k(-1)^{k - i}(^k_i)f_i ans=i=1k(1)ki(ik)fi

然而事实上。。这个容斥原理我想了很久很久也没搞懂这到底是个啥。。虽然感性上好像是这么回事,但是总感觉有点不太能说服自己…由于仍在学习组合数学没有系统学完容斥原理因而说不出所以然qwq
又一次加深了我赶紧学完组合数学的决心

贴个代码日后想起来可能会补坑

const int M = 998244353;
const int maxn = 5e3 + 10;

int n, k;
ll dp[maxn][maxn], suf[maxn];
ll inv[maxn];
ll getinv(ll a){
    ll x, y; exgcd(a, M, x, y);
    return (x % M + M) % M;
}

void update(int row){
    suf[k - 1] = dp[row][k - 1] % M;
    for(int i = k - 2; i >= 0; i--) suf[i] = (dp[row][i] + suf[i + 1]) % M;
}

int sgn(int x){return x & 1? -1: 1;}

ll qp(int x, int p){
    ll res = x, ans = 1;
    while(p){
        if(p & 1) ans = ans * res % M;
        res = (res % M) * (res % M) % M;
        p >>= 1;
    }
    return ans;
}

ll C(int n, int m){
    ll ans = 1;
    m = min(m, n - m);
    for(int i = 1; i <= m; i++){
        ans = ans * (n + 1 - i) % M;
        ans = ans * inv[i] % M;
    }
    return ans % M;
}

int main(){
//    Fast;
    for(int i = 1; i < maxn; i++) inv[i] = getinv(i);
    scanf("%d %d", &n, &k);
    
    dp[0][0] = 1; update(0);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j < k; j++){
            dp[i][j] = dp[i - 1][j - 1] * (k - j + 1) % M + suf[j];
            dp[i][j] %= M;
        }
        update(i);
    }
    ll ans = 0;
    for(int i = 1; i < k; i++) ans = (ans + dp[n][i]) % M;
    
    for(int i = k - 1; i >= 1; i--){
        ans += sgn(k - i) * C(k, i) * qp(i, n) % M;
        ans %= M;
    }
    printf("%lld\n", (ans % M + M) % M);
    return 0;
}

打网络赛的时候感觉自己状态不是很好…而且发现好多基础知识点其实是自己的盲区/软肋…CF训练多了好像就可能会出现这种严重的偏科现象(然而偏啥了仔细想想好像除了偏手速&切简单题就没有偏什么真正的好处了

另外在此之前可能大概有大半个月没怎么训练过了…之前几周没干什么好事…时间利用上也相当不充分…专业课一脚一个坑…然而还是…奋起学习吧! 绝望之为虚妄,正为希望相同。

你可能感兴趣的:(ACM)