【解题报告】2021牛客寒假算法基础集训营 6

前面的话

  • 2021牛客寒假算法基础集训营 6
    这场真的是 ⌈ \lceil 命途多舛 ⌋ \rfloor A A A坑题, H H H暴力能过,签到题还挺多…
    以后我签到题写个 Q D QD QD 吧,就只给个题意和思路吧,不然长度太长不容易翻阅。

A \mathfrak{A} A :回文括号序列计数 | QD思维

  • 【题意】
    问长度为 n n n回文合法括号序列的个数
  • 【范围】
    样例组数 T ≤ 1 0 6 T\le 10^6 T106
    0 ≤ n ≤ 1 0 9 0\le n\le 10^9 0n109
  • 【思路】赛内
    问:以下哪个括号串是回文的
    A: ( ( ) ( ) ) (()()) (()())
    B: ( ( ) ( ( ) (()(() (()(()
    C: ( ( ) ) ( ( (())(( (())((
    答案是 c \tiny{c} c A A A镜像串 S y m m e t r y   S t r i n g Symmetry\ String Symmetry String
    那就送分题了。第一个位置为 ) ) ) 或者 ( ( ( 都会导致结果不合法,只有 n = 0 n=0 n=0时才为 1 1 1(空串)
    顺带一提,如果题目问的是镜像串的数量,那么首先判断 n n n 必须为偶数,然后 n = n / 2 n=n/2 n=n/2,最终答案为 C n ⌊ n / 2 ⌋ C_n^{\lfloor n/2\rfloor} Cnn/2

B \mathfrak{B} B :系数 | 数学

  • 【题意】
    问你 ( x 2 + x + 1 ) n (x^2+x+1)^n (x2+x+1)n 的第 k k k 项系数取模 3 3 3 是多少。
  • 【范围】
    样例组数 T ≤ 1 0 4 T\le 10^4 T104
    0 ≤ n ≤ 1 0 15 0\le n\le 10^{15} 0n1015
    0 ≤ k ≤ 2 × n 0\le k\le 2\times n 0k2×n
  • 【思路】题解 + 赛后
    如果直接去做的话,是一个 O ( n k ) O(nk) O(nk) d p dp dp,式子如下:
    d p [ i ] [ j ] = { 1 j = 0 或 者 j = 2 i d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] j = 1 d p [ i − 1 ] [ j − 2 ] + d p [ i − 1 ] [ j − 1 ] j = 2 i − 1 d p [ i − 1 ] [ j − 2 ] + d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j ] O t h e r s dp[i][j]= \begin{cases} 1& j=0 或者 j=2i\\ dp[i-1][j-1]+dp[i-1][j]&j=1\\ dp[i-1][j-2]+dp[i-1][j-1]&j=2i-1\\ dp[i-1][j-2]+dp[i-1][j-1]+dp[i-1][j]&Others \end{cases} dp[i][j]=1dp[i1][j1]+dp[i1][j]dp[i1][j2]+dp[i1][j1]dp[i1][j2]+dp[i1][j1]+dp[i1][j]j=0j=2ij=1j=2i1Others
    应该没写错吧
    这是一个不规则三角阵,貌似没法直接去做…有题解看出来是一个分形,进行具体分析的 % % % \%\%\% %%%
    我们注意到一个细节: 输 出 取 模 3 的 系 数 输出取模3的系数 3,原式子为 ( x 2 + x + 1 ) n (x^2+x+1)^n (x2+x+1)n
    考虑同余,我们知道 ( x 2 + x + 1 ) n ≡ ( x 2 − 2 x + 1 ) n ≡ ( x − 1 ) 2 n ( m o d 3 ) (x^2+x+1)^n\equiv (x^2-2x+1)^n\equiv (x-1)^{2n}\pmod 3 (x2+x+1)n(x22x+1)n(x1)2n(mod3)
    然后直接二项式,答案就得到了:
    ( − 1 ) 2 n − k C 2 n k (-1)^{2n-k}C_{2n}^k (1)2nkC2nk
    因为模数为 3 3 3 是一个质数,直接用 L u c a s Lucas Lucas 定理做就行了。
  • 【代码】
    时间复杂度: T log ⁡ n T\log n Tlogn
    32 / 1000 M s 32/1000Ms 32/1000Ms
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 1e5+50;

ll MOD;
ll qpow(ll a,ll n){/* */ll res = 1LL;while(n){if(n&1)res=res*a%MOD;a=a*a%MOD;n>>=1;}return res;}
ll inv(ll a){/* */return qpow(a,MOD-2);}

ll fac[MAX],ivfac[MAX];
void init(int n){
    fac[0] = 1;
    for(int i = 1;i <= n;++i)fac[i] = fac[i-1] * i % MOD;
    ivfac[n] = inv(fac[n]);
    for(int i = n-1;i >= 0;--i)ivfac[i] = ivfac[i+1]*(i+1)%MOD;
}
ll C(ll n,ll m){
    if(m > n)return 0;
    return fac[n] * ivfac[m] % MOD * ivfac[n - m] % MOD;
}
ll Lucas(ll n,ll m){
    if(m == 0)return 1;
    return C(n % MOD,m % MOD) * Lucas(n / MOD, m / MOD) % MOD;
}
int main()
{
    ll n,k;
    int T;cin >> T;
    while(T--){
        cin >> n >> k;
        MOD = 3;
        init(MOD - 1);
        int sgn = 1;
        if((2*n-k)&1)sgn = -1;
        ll res = (Lucas(2*n,k) * sgn % MOD + MOD) % MOD;
        cout << res << endl;
    }
    return 0;
}

C \mathfrak{C} C : 末三位 | QD数学

  • 【题意】
    5 n 5^n 5n 的末三位为多少
  • 【范围】
    样例组数 T ≤ 1 0 6 T\le 10^6 T106
    0 ≤ n ≤ 1 0 9 0\le n\le 10^9 0n109
  • 【思路】
    对于任意 x x x x n x^n xn的末三位都有循环节的。 x = 5 x=5 x=5 的循环节如下:
    005 , 025 , 125 , 625 , 125 , 625 ⋯ 005,025,125,625,125,625\cdots 005,025,125,625,125,625

D \mathfrak{D} D : 划数 | QD数学

  • 【题意】
    n n n 个数,每次划去两个数 x , y x,y xy,增加一个数 ( x + y ) % 11 (x+y)\%11 (x+y)%11
    划到只剩两个数时,一个数为 c n t cnt cnt,问你另个一数为几。
  • 【范围】
    c n t ≥ 11 cnt\ge11 cnt11
    2 ≤ n ≤ 15000 2\le n\le 15000 2n15000
    1 ≤ n u m [ i ] ≤ 1 0 6 1\le num[i]\le 10^6 1num[i]106
  • 【思路】
    剩下的那个 c n t cnt cnt 肯定是没划过的。
    n = 2 n=2 n=2,剩下的就是另一个数字。
    n > 2 n>2 n>2,剩下的就是其他所有划过的数字。等于其他数字的总和取模 11 11 11

E \mathfrak{E} E :网格 | d p dp dp

  • 【题意】
    n × m n\times m n×m 的网格,每个位置有一个权值 a i , j a_{i,j} ai,j,每个位置要从上下左右方向选择互相垂直的两个
    定义 w ( x ) = x + p o p c n t ( x ) w(x)=x+popcnt(x) w(x)=x+popcnt(x),其中 p o p c n t ( x ) popcnt(x) popcnt(x) 表示 x x x 的二进制表示中 1 1 1 的个数。
    如果两个相邻的位置 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 互相位与对方选择的方向之中,那么答案增加 w ( a x 1 , y 1 ⊕ a x 2 , y 2 ) w(a_{x_1,y_1}\oplus a_{x_2,y_2}) w(ax1,y1ax2,y2),其中 ⊕ \oplus 表示二进制按位异或。
    问你答案最大值为多少。
  • 【范围】
    1 ≤ n , m ≤ 1000 1\le n,m\le 1000 1n,m1000
    0 ≤ a i , j < 1024 0\le a_{i,j}<1024 0ai,j<1024
  • 【思路】题解
    赛内的时候感觉:方向好复杂! w ( x ) w(x) w(x) 好复杂!贡献还要异或!一堆给人不可做的感觉…
    但是我们把要求 ⌈ \lceil 抽丝剥茧 ⌋ \rfloor 之后,就没有那么复杂了。
    方向选择的要求是上下左右方向选择互相垂直的两个,我们改成:向左和向右选择一个方向,向上和向下选择一个方向
    先考虑左右方向。那么对答案的贡献只能是一行里面某两个相邻位置对答案有贡献。
    这不就是最简单的 d p dp dp 吗?设 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示当前行第 i i i 列选择方向向左, d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示选择方向向右。
    d p [ i ] [ 1 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] } d p [ i ] [ 0 ] = max ⁡ { d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] + w ( 贡 献 ) } \begin{aligned} dp[i][1]&=\max\{dp[i-1][0],dp[i-1][1]\}\\ dp[i][0]&=\max\{dp[i-1][0],dp[i-1][1]+w(贡献)\} \end{aligned} dp[i][1]dp[i][0]=max{dp[i1][0],dp[i1][1]}=max{dp[i1][0],dp[i1][1]+w()}
    这个贡献,我们只要预处理好每个数字的 p o p c n t [ i ] popcnt[i] popcnt[i] 即可。
    对于上下方向,和该转移式子差别不大。
  • 【代码】
    时间复杂度: O ( n m ) O(nm) O(nm)
    19 / 3000 M s 19/3000Ms 19/3000Ms
const int MAX = 1e3+50;

int pop[MAX];
int aa[MAX][MAX];
ll dp[MAX][2];
int lowbit(int x){
    return (x & -x);
}
void init(int n){
    pop[0] = 0;
    for(int i = 1;i <= n;++i){
        pop[i] = pop[i-lowbit(i)] + 1;
    }
}
int main()
{
    init(1024);
    int n,m;n = read();m = read();
    for(int i = 1;i <= n;++i)
        for(int j = 1;j <= m;++j)
            aa[i][j] = read();
    ll res = 0;
    for(int i = 1;i <= n;++i){
        dp[1][0] = dp[1][1] = 0;
        for(int j = 2;j <= m;++j){
            ll tmp = aa[i][j] ^ aa[i][j-1];
            ll duo = tmp + pop[tmp];
            dp[j][1] = max(dp[j-1][0],dp[j-1][1]);
            dp[j][0] = max(dp[j-1][0],dp[j-1][1] + duo);
        }
        res += dp[m][0];
    }
    for(int i = 1;i <= m;++i){
        dp[1][0] = dp[1][1] = 0;
        for(int j = 2;j <= n;++j){
            ll tmp = aa[j][i] ^ aa[j-1][i];
            ll duo = tmp + pop[tmp];
            dp[j][1] = max(dp[j-1][0],dp[j-1][1]);
            dp[j][0] = max(dp[j-1][0],dp[j-1][1] + duo);
        }
        res += dp[n][0];
    }
    printf("%lld",res);
    return 0;
}

F \mathfrak{F} F :组合数问题 | 数学 + 打表

  • 【题意】
    4 ∣ n 4|n 4n,求计算 C n 0 + C n 4 + C n 8 + ⋯ + C n n C_n^0+C_n^4+C_n^8+\cdots+C_n^n Cn0+Cn4+Cn8++Cnn
  • 【范围】
    1 ≤ n ≤ 1 0 18 1\le n\le 10^{18} 1n1018
  • 【思路】赛内
    前几项打表,得出式子: t = n / 4 t=n/4 t=n/4
    O E I S OEIS OEIS 之后得出答案:
    a n s = ( − 4 ) t × 1 2 + 1 6 n × 1 4 ans=(-4)^t\times \frac{1}{2}+16^n\times \frac{1}{4} ans=(4)t×21+16n×41
    虽然有点偷懒

G \mathfrak{G} G : 机器人 | 状压 d p dp dp + 高精度

  • 【题意】
    n n n 个机器,第 i i i 个入读一个数字 x x x 就会输出 a i x + b i a_ix+b_i aix+bi
    你一开始有一个数字 x x x,每个机器只用一次,最终最大值为多少。
  • 【范围】
    1 ≤ a i , b i , x , n ≤ 20 1\le a_i,b_i,x,n\le 20 1ai,bi,x,n20
  • 【思路】赛内
    2 0 20 20^{20} 2020 l l ll ll 了,我最后用了 J a v a Java Java 大数写,虽然也可以 _ _ i n t 128 \_\_int128 __int128
    一个简单的状压 d p dp dp,假设现在的机器人集合为 i i i d p [ i ] dp[i] dp[i] 表示选择完这个集合的机器人,输出的最大值
    d p [ i ] = max ⁡ { d p [ i − j ] × a j + b j } i & j = j , j = l o w b i t ( j ) dp[i]=\max\{dp[i-j]\times a_j+b_j\}\qquad i\&j=j,j=lowbit(j) dp[i]=max{dp[ij]×aj+bj}i&j=j,j=lowbit(j)
    很好懂的吧…
  • 【代码】
    时间复杂度: O ( 2 n × n ) O(2^{n}\times n) O(2n×n)
    2645 / 6000 M s 2645/6000Ms 2645/6000Ms
import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    static BigInteger[] A = new BigInteger[30];
    static BigInteger[] B = new BigInteger[30];
    static BigInteger[] dp = new BigInteger[1100000];
    static int[] pw = new int[30];
    public static void main(String[] args){
        Scanner read = new Scanner(System.in);
        int n = read.nextInt();
        BigInteger x = read.nextBigInteger();
        int ed = 1;
        for(int i = 0;i < n;++i){
            A[i] = read.nextBigInteger();
            B[i] = read.nextBigInteger();
            pw[i] = ed;
            ed = ed * 2;
        }
        ed--;
        dp[0] = x;
        for(int i = 1;i <= ed;++i){
            dp[i] = BigInteger.ZERO;
            for(int j = 0;j < n;++j){
                if((i & pw[j]) > 0){
                    int re = i - pw[j];
                    BigInteger tmp = dp[re].multiply(A[j]);
                    tmp = tmp.add(B[j]);
                    dp[i] = dp[i].max(tmp);
                }
            }
        }
        System.out.println(dp[ed]);
    }
}
  • 【补充】
    这题也可以贪心排序,不过这个数据,出题人的本意是让我们做状压 d p dp dp 的。

H \mathfrak{H} H :动态最小生成树

  • 赛后看别人代码:暴力能过?暴力能过!暴力能过…
    正解显然我不会。

I \mathfrak{I} I :贪吃蛇 | QD B F S BFS BFS

  • 【题意】
    走迷宫,蛇每走一格,就长长一格。
  • 【范围】
    1 ≤ n , m ≤ 100 1\le n,m\le 100 1n,m100
  • 【思路】赛内
    走迷宫,就是无法走回头路,还是 B F S BFS BFS 就能搞定。
  • 【补充】
    这题无法到达输出 − 1 -1 1

J \mathfrak{J} J : 天空之城 | K r u s k a l Kruskal Kruskal M S T MST MST

  • 【题意】
    n n n 个城市, q q q 条边。每个边的权值表示走该路的花费时间。
    给定一个起点 s s s。你希望走过所有城市,且花费时间最短。
    你走过的边再走就会不花时间。
  • 【范围】
    1 ≤ n ≤ 5000 1\le n\le 5000 1n5000
    1 ≤ q ≤ 200000 1\le q\le 200000 1q200000
    1 ≤ v a l ≤ 1 0 9 1\le val\le 10^9 1val109
  • 【思路】赛内
    城市名字字符串转成编号。
    走过的边再走不会花时间?那就是一个 M S T MST MST 的裸题。
    写个 并查集然后用 K r u s k a l Kruskal Kruskal 即可。
  • 【代码】
    时间复杂度: O ( q log ⁡ q ) O(q\log q) O(qlogq)
    425 / 5000 M s 425/5000Ms 425/5000Ms
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
const int MAX = 2e5+50;

unordered_map<string,int>M;
struct node{
    int ta,tb;
    ll w;
    bool operator <(const node &ND)const{
        return w < ND.w;
    }
}aa[MAX];
int fa[MAX];
int find_fa(int x){
    if(x == fa[x])return x;
    return fa[x] = find_fa(fa[x]);
}
bool sam(int x,int y){
    int fx = find_fa(x);
    int fy = find_fa(y);
    return fx == fy;
}
void add(int x,int y){
    int fx = find_fa(x);
    int fy = find_fa(y);
    if(fx != fy){
        fa[fx] = fy;
    }
}

int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m)){
        int cnt = 0;
        M.clear();
        for(int i = 1;i <= n;++i){
            fa[i] = i;
        }
        string st;cin >> st;
        for(int i = 1;i <= m;++i){
            string ta,tb;ll q;
            cin >> ta >> tb >> q;
            int sa,sb;
            if(!M[ta])M[ta] = ++cnt;
            if(!M[tb])M[tb] = ++cnt;
            sa = M[ta];
            sb = M[tb];
            aa[i].ta = sa;
            aa[i].tb = sb;
            aa[i].w = q;
        }
        sort(aa+1,aa+1+m);
        ll res = 0;
        for(int i = 1;i <= m;++i){
            int ta = aa[i].ta;
            int tb = aa[i].tb;
            if(!sam(ta,tb)){
                add(ta,tb);
                res += aa[i].w;
            }
        }
        bool can = true;
        for(int i = 2;i <= n;++i){
            if(!sam(i,1)){
                can = false;
                break;
            }
        }
        if(!can)puts("No!");
        else printf("%lld\n",res);
    }
    return 0;
}
  • 【补充】
    这题无法到达输出 N o ! No! No!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值