cf 比赛 04

2021.04.30

训练地址

B. The Number of Pairs

在这里插入图片描述

  • u = g c d ( a , b ) u = gcd(a, b) u=gcd(a,b),设 l c m ( a , b ) = k ∗ u lcm(a, b) = k * u lcm(a,b)=ku,则原式可写为 u ∗ ( k ∗ c − d ) = x u * (k * c - d) = x u(kcd)=x,那么有整数解,必须要满足 u ∣ x u | x ux. 则我们只需要枚举 x x x 的约数,这样子,就可以求出 k k k 了。
  • k = x / u + d c k = \frac{x/u+d}{c} k=cx/u+d. 那么,我们确定了 g c d gcd gcd l c m lcm lcm,这样子的话, ( a , b ) (a,b) (a,b) 的组合数就是 2 l c m / g c d 的 质 因 数 个 数 2^{lcm / gcd的质因数个数} 2lcm/gcd.
  • 那么我们先晒出来每个数质因数的个数即可,这个地方还挺容易错的,就是在计算 s t [ i ∗ p r i m e [ j ] ] st[i * prime[j]] st[iprime[j]] 的地方的时候,要分 i % p r i m e [ j ] = = 0 i \% prime[j] == 0 i%prime[j]==0 讨论。因为每一个质数只能算入一次。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 20000010;
int prime[N], cnt;
int st[N];
ll pw[100] = {1};

void sieve(int n)
{
    for(int i = 2; i <= n; i++){
        if(!st[i]){
            prime[cnt++] = i;
            st[i] = 1;
        }
        for(int j = 0; prime[j] <= n / i; j++){
            if(i % prime[j] == 0) st[i * prime[j]] =  st[i];
            else st[i * prime[j]] = st[i] + 1;
            if(i % prime[j] == 0) break;
        }
    }
    for(int i = 1; i < 30; i++) pw[i] = pw[i - 1] * 2;
}
vector<int> divisor(ll n)
{
    vector<int> res;
    for(int i = 1; i <= n / i; i++){
        if(n % i == 0){
            res.push_back(i);
            if(n / i != i) res.push_back(n / i);
        }
    }

    return res;
}
int main()
{
    sieve(N - 1);
    int T;
    scanf("%d", &T);

    while(T--){
        ll ans = 0;
        ll a, b, c;
        scanf("%lld%lld%lld", &a, &b, &c);
        vector<int> res = divisor(c);
        for(auto p : res){
            ll tmp = c / p + b;
            if(tmp % a) continue;
            ll k = tmp / a;
            ans += pw[st[k]];
        }

        printf("%lld\n", ans);
    }
    return 0;
}

C. Chaotic Merge

  • 题意:
    在这里插入图片描述
  • 题解:
  • 其实这个题相当于有很多dp的起点(拓扑图的起点),那么可以边递推边加入新的起点.
  • 而底下不断加入 d p x i , d p y j dpx_i, dpy_j dpxi,dpyj,就是不断加入新的起点的过程. 比如 d p ( i , j , 0 ) + = d p y j dp(i, j, 0) += dpy_j dp(i,j,0)+=dpyj 就是表示加入了一个一串 y 中的连续字符串加上一个 x i x_i xi 中的字符串作为一个起点.
    在这里插入图片描述
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
typedef long long ll;
const ll mod = 998244353;
char x[N], y[N];
ll f[N][N][2], fx[N], fy[N];
int main()
{
    scanf("%s%s", x + 1, y + 1);
    int n = strlen(x + 1);
    int m = strlen(y + 1);
    for(int i = 1; i <= n; i++){
        if(x[i] != x[i - 1]) fx[i] = fx[i - 1] + 1;
        else fx[i] = 1;
    }
    for(int i = 1; i <= m; i++){
        if(y[i] != y[i - 1]) fy[i] = fy[i - 1] + 1;
        else fy[i] = 1;
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(x[i] != y[j]){
                f[i][j][0] = (f[i][j][0] + fy[j]) % mod;
                f[i][j][1] = (f[i][j][1] + fx[i]) % mod;
            }
            if(x[i] != x[i - 1]){
                f[i][j][0] = (f[i][j][0] + f[i - 1][j][0]) % mod;
            }
            if(x[i] != y[j]){
                f[i][j][0] = (f[i][j][0] + f[i - 1][j][1]) % mod;
                f[i][j][1] = (f[i][j][1] + f[i][j - 1][0]) % mod;
            }
            if(y[j] != y[j - 1]){
                f[i][j][1] = (f[i][j][1] + f[i][j - 1][1]) % mod;
            }
        }
    }
    ll ans = 0;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            for(int k = 0; k <= 1; k++){
                ans = (ans + f[i][j][k]) % mod;
            }
        }
    }
    printf("%lld\n", ans);
    return 0;
}

D. Diameter Cuts

  • 给一棵树,让你删掉一些边,使得生成的森林的每棵树的直径都不超过k. 问删除的边组成的集合,有多少这样的集合。
  • f ( u , l e n ) f(u, len) f(u,len) 表示以 u 为根节点的子树,那么我们可以两两子树遍历, f ( u , l e n ) + = f ( v 1 , i ) + f ( v 2 , j ) ,   i + 1 + j + 1 ≤ k . f(u, len) += f(v_1, i) + f(v_2, j),\quad \ i+1 + j + 1 \le k. f(u,len)+=f(v1,i)+f(v2,j), i+1+j+1k.
  • 但其实我们可以依次枚举子结点,看它对答案的贡献,这也是一个常见技巧了,具体可以看代码. 而且我们知道, f ( v , j ) f(v, j) f(v,j) 中的 j j j 不可能超过子树的深度.
#include<bits/stdc++.h>
using namespace std;
const int N = 5010, M = 2 * N;
typedef long long ll;
const ll mod = 998244353;
int n, k;
ll f[N][N];
int h[N], e[M], ne[M], idx;
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u, int fa)
{
    int height = 0;
    f[u][0] = 1;
    for(int i = h[u]; i != -1; i = ne[i]){
        int v = e[i];
        if(v == fa) continue;
        int nh = dfs(v, u);
        vector<int> tmp(max(height, nh + 1) + 1);
        for(int i = 0; i <= height; i++){
            for(int j = 0; j <= nh; j++){
                if(i + j + 1 <= k){
                    tmp[max(i, j + 1)] = (tmp[max(i, j + 1)] + f[u][i] * f[v][j] % mod) % mod;
                }
                if(i <= k){
                    tmp[i] = (tmp[i] + f[u][i] * f[v][j]) % mod;
                }
            }
        }
        height = max(height, nh + 1);
        for(int i = 0; i <= height; i++){
            f[u][i] = tmp[i];
        }
    }
    return height;
}
int main()
{
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &k);
    for(int i = 1; i < n; i++){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    dfs(1, -1);
    ll ans = 0;
    for(int i = 0; i <= k; i++){
        ans = (ans + f[1][i]) % mod;
    }
    printf("%lld\n", ans);
    return 0;
}

G. Genius

  • 题意:有n个题,从前往后的复杂度 c i c_i ci 2 i 2^i 2i,我们如果刚解决问题 j j j,想要解决问题 i i i,就要满足 I Q < ∣ c i − c j ∣ IQ<|c_i-c_j| IQ<cicj,解完 i i i 后, I Q IQ IQ 就会变为 ∣ c i − c j ∣ |c_i-c_j| cicj,并得到 ∣ s i − s j ∣ |s_i-s_j| sisj 分,刚开始 I Q IQ IQ 0 0 0,我们可以先解任意一个题,一个题可以解决多次,我们要得出最多可以有多少分.
  • 我们可以证明,任意两点间的 ∣ c i − c j ∣ |c_i - c_j| cicj 的数值是不同的. 假设 i > j i > j i>j,因为 ∣ 2 i − 2 j ∣ |2^i - 2^j| 2i2j 是从 j j j i − 1 i - 1 i1 位全是1,其他全是0.
  • 我们还发现,如果我们随意走出一条路径,那么我们只需要调整一下走的顺序,就是一条合法路径。因此我们只需要枚举 ( i , j ) (i, j) (i,j) 即可.
  • 最后,我们定义 f i f_i fi 表示在第 i 个点结束做题时的答案,我们第一维枚举结束的顶点,第二维倒着枚举,从 i − 1 i - 1 i1 到 1,这样子一定可以保证最后走的那条路是最大值.
#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
typedef long long ll;
ll f[N], tag[N], s[N];
int main()
{
    int T;
    scanf("%d", &T);
    while(T--){
        int n;
        scanf("%d", &n);
        memset(f, 0, sizeof f);
        for(int i = 1; i <= n; i++){
            scanf("%lld", &tag[i]);
        }
        for(int i = 1; i <= n; i++){
            scanf("%lld", &s[i]);
        }

        for(int i = 1; i <= n; i++){
            for(int j = i - 1; j >= 1; j--){
                if(tag[i] == tag[j]) continue;
                ll p = abs(s[i] - s[j]), fi = f[i], fj = f[j];
                f[i] = max(f[i], fj + p), f[j] = max(f[j], fi + p);
            }
        }
        printf("%lld\n", *max_element(f + 1, f + n + 1));
    }
    return 0;
}

H. Square-free division (easy version)

在这里插入图片描述

  • 这个题想复杂了。我们把所有的数字的质因数的幂都模2,这样子的话,我们从前往后遍离数组,并用一个map记录之前哪些数字出现过。
  • 如果新加进来的改变的数字并不存在于 map 中,说明当前数字加入当前区间不会引起冲突,于是把这个数字加入当前的区间之中。
  • 如果之前这个数出新过,那么就在这个数前面画一条分界线,把map清空,把这个数加到map里面,然后答案加1.
#include<bits/stdc++.h>
using namespace std;
const int N = 10000010;
int prime[N], cnt, st[N];
void sieve(int n)
{
    for(int i = 2; i <= n; i++){
        if(!st[i]) prime[cnt++] = st[i] = i;
        for(int j = 0; prime[j] <= n / i; j++){
            st[i * prime[j]] = prime[j];
            if(i % prime[j] == 0) break;
        }
    }
}
int change(int x)
{
    int res = 1;
    while(x > 1){
        int u = st[x];
        int cnt = 0;
        while(x % u == 0){
            cnt++;
            x /= u;
        }
        if(cnt & 1) res *= u;
    }
    return res;
}
int main()
{
    sieve(N - 1);
    int T;
    scanf("%d", &T);

    while(T--){
        set<int> S;
        int n, k;
        scanf("%d%d", &n, &k);
        int ans = 1, tmp;
        for(int i = 1; i <= n; i++){
            scanf("%d", &tmp);
            tmp = change(tmp);
            if(S.count(tmp)){
                ans++;
                S.clear();
            }
            S.insert(tmp);
        }
        printf("%d\n", ans);
    }
    return 0;
}

I. Square-free division (hard version)

  • 给一个序列 n ≤ 2 e 5 n \le 2e5 n2e5,问最少分割成几个连续字段,使得每个连续字段中不存在两个数的积是完全平方数. 可以改变中间的某个数字至多 k ( k ≤ 20 ) k(k \le 20) k(k20) 次,改为任意的数字即可.
  • 我们先处理出 L e f t ( i , j ) Left(i,j) Left(i,j) 表示从 i i i 开始,可以删掉 k k k j j j 个数字,满足 [ l , i ] [l,i] [l,i] 中间是合法连续子序列的最往左的左区间端点是多少. 这个可以用双指针在 O ( n k ) O(nk) O(nk) 的时间内解决.
  • 然后令 f ( i , j ) f(i,j) f(i,j) 表示前 i i i 个数字,改变 j j j 个数字的最少划分数是多少. 这个我们可以枚举 x ∈ [ 0 , j ] x \in [0,j] x[0,j] l = L e f t ( i , j ) l = Left(i,j) l=Left(i,j),可以分为从两块,及 [ 1 , l − 1 ] [1, l - 1] [1,l1] 的最优划分加上 [ l , i ] [l,i] [l,i] 作为一个完整连续子段,并且前者改变 j − x j - x jx 次,后者改变 x x x 次.
    f ( i , j ) = min ⁡ { f ( i , j ) , f ( l − 1 , j ) + 1 } f(i,j) = \min\{f(i,j),f(l-1,j) + 1\} f(i,j)=min{f(i,j),f(l1,j)+1}
  • 初始化的时候, f f f初始化为正无穷, f ( 0 , 0 ) f(0,0) f(0,0) 初始化为1即可.
#include<bits/stdc++.h>
using namespace std;
const int N = 200010, M = 10000010;
int prime[M], st[M], cnt;
void sieve(int n)
{
    for(int i = 2; i <= n; i++){
        if(!st[i]) prime[cnt++] = st[i] = i;
        for(int j = 0; prime[j] <= n / i; j++){
            st[i * prime[j]] = prime[j];
            if(i % prime[j] == 0) break;
        }
    }
}
int change(int x)
{
    vector<int> res;
    while(x > 1){
        int u = st[x];
        int cnt = 0;
        while(x % u == 0){
            x /= u;
            cnt++;
        }
        if(cnt & 1) res.push_back(u);
    }
    int ans = 1;
    for(auto p : res){
        ans = ans * p;
    }
    return ans;
}
int a[N], n, k, Left[N][30], f[N][30];
unordered_map<int, int> Map(N);
int main()
{
    sieve(M - 1);
    int T;
    scanf("%d", &T);
    while(T--){
        scanf("%d%d", &n, &k);
        for(int i = 1; i <= n; i++){
            int x;
            scanf("%d", &x);
            a[i] = change(x);
        }
        for(int i = 1; i <= n; i++){
            for(int j = 0; j <= k; j++){
                Left[i][j] = 0;
                f[i][j] = 1e9;
            }
        }
        f[0][0] = 0;
        for(int lim = 0; lim <= k; lim++){
            Map.clear();
            int cnt = 0;
            for(int i = 1, j = 1; i <= n; i++){
                Map[a[i]]++;
                if(Map[a[i]] > 1) cnt++;
                while(i >= j && cnt > lim){
                    if(Map[a[j]] > 1){
                        cnt--;
                    }
                    Map[a[j]]--;
                    j++;
                }
                Left[i][lim] = j;
            }
        }

        for(int i = 1; i <= n; i++){
            for(int j = 0; j <= k; j++){
                for(int x = 0; x <= j; x++){
                    int l = Left[i][x];
                    f[i][j] = min(f[i][j], f[l - 1][j - x] + 1);
                }
            }
        }
        int ans = 1e9;
        for(int j = 0; j <= k; j++){
            ans = min(ans, f[n][j]);
        }
        printf("%d\n", ans);
    }
    return 0;
}

Codeforces Global Round 14

训练地址

E. Phoenix and Computers

  • 给了一排电脑 n ≤ 400 n \le 400 n400,最开始全是关闭状态,可以一个一个打开它. 当打开了 i − 1 i - 1 i1 i + 1 i + 1 i+1 的电脑的时候,第 i i i 个电脑也会自动打开.
  • f ( l e n , c n t ) f(len, cnt) f(len,cnt) 表示点亮了前 l e n − 1 len - 1 len1 个电脑,手动打开了 c n t cnt cnt 个,假如后面又打开了 k k k 个连续段,那么状态转移方程是
    f ( l e n + k + 1 , c n t + k ) + = f ( l e n , c n t ) ∗ C l e n + c n t c n t ∗ 2 k − 1 . f(len + k + 1, cnt + k) += f(len, cnt) * C_{len + cnt}^{cnt} * 2 ^{k - 1}. f(len+k+1,cnt+k)+=f(len,cnt)Clen+cntcnt2k1.
  • 一个需要注意的地方是,dp 的起点,就是 f ( 0 , 0 ) = 1 f(0, 0) = 1 f(0,0)=1. 因为从 ( 0 , 0 ) (0, 0) (0,0) 出发可以到达任何状态.
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 510;
ll n, mod;
ll f[N][N], pw[N], fact[N], infact[N];
ll mod_pow(ll x, ll n)
{
    ll res = 1;
    while(n){
        if(n & 1) res = res * x % mod;
        x = x * x % mod;
        n >>= 1;
    }
    return res;
}
ll C(ll a, ll b)
{
    return fact[a] * infact[a - b] % mod * infact[b] % mod;
}

int main()
{
    scanf("%lld%lld", &n, &mod);
    fact[0] = infact[0] = 1;
    for(ll i = 1; i < N; i++){
        fact[i] = i * fact[i - 1] % mod;
        infact[i] = infact[i - 1] * mod_pow(i, mod - 2) % mod;
    }
    pw[0] = 1;
    for(int i = 1; i < N; i++){
        pw[i] = pw[i - 1] * 2 % mod;
    }
    f[0][0] = 1;

    for(int i = 0; i <= n + 1; i++){
        for(int j = 0; j <= i; j++){
            for(int k = 1; k <= n + 1; k++){
                if(i + k + 1 > n + 1) break;
                f[i + k + 1][j + k] = (f[i + k + 1][j + k] + f[i][j] * C(j + k, k) % mod * pw[k - 1] % mod) % mod;
            }
        }
    }
    ll ans = 0;
    for(int i = 1; i <= n; i++){
        ans = (ans + f[n + 1][i]) % mod;
    }
    printf("%lld\n", ans);
    return 0;
}

2021.05.03

训练地址

B. Two chandeliers

G. Garden of the Sun

  • 题意:给一个矩阵,上面有些格子是 ‘X’,有些格子是 ‘.’,并且保证所有的 ‘X’ 都是非八连通的。现在要加一些 ‘X’ 使得他们成为四联通,并且没有环
  • 做法:充分利用非八连通的性质,从第一行开始,每隔两行,就把这一行全部涂成 ‘X’,然后中间那两行,如果出现了 ‘X’,就把上下两个格子都填成 ‘X’,然后 break. 如果没有出现过 ‘X’,就把第一列的两个格子填成 ‘X’.
  • 但是,有一个特殊情况,就是 n % 3 == 0 的情况,这个最后两行可能会出现不连通的情况。那么就把最后两行只要出现 ‘X’,就把上下两个格子填成 ‘X’.
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
char s[N][N];
int main()
{
    int T;
    scanf("%d", &T);
    while(T--){
        int n, m;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++){
            scanf("%s", s[i] + 1);
        }
        for(int i = 1; i <= n; i += 3){
            if(i % 3 == 1){
                for(int j = 1; j <= m; j++) s[i][j] = 'X';
            }
            if(i + 2 <= n){
                bool flag = true;
                //printf("***\n");
                for(int j = 1; j <= m && flag; j++){
                    if(s[i + 1][j] == 'X' || s[i + 2][j] == 'X'){
                        s[i + 1][j] = s[i + 2][j] = 'X';
                        flag = false;
                        //printf("*** %d %d\n", i, j);
                    }
                }
                if(flag) s[i + 1][1] = s[i + 2][1] = 'X';
            }
        }
        if(n % 3 == 0){
            for(int j = 1; j <= m; j++){
                if(s[n - 1][j] == 'X' || s[n][j] == 'X'){
                    s[n - 1][j] = s[n][j] = 'X';
                }
            }
        }
        for(int i = 1; i <= n; i++) s[i][m + 1] = 0;
        for(int i = 1; i <= n; i++){
            printf("%s\n", s[i] + 1);
        }
    }
    return 0;
}

H. BFS Trees

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值