【解题报告】Codeforces Global Round 13 | ABCDE

A:K-th Largest Value | QD

B:Minimal Cost | 思维

  • 【题意】
    在这里插入图片描述
    i i i 行障碍物的位置为 a i a_i ai
    把一个障碍物水平方向移动一格花费 v v v,垂直移动一格花费 u u u
    问你能从 ( 1 , 0 ) (1,0) (1,0) 移动到 ( n , 1 0 6 + 1 ) (n,10^6+1) (n,106+1) 的最小障碍物移动花费为多少。
  • 【范围】
    2 ≤ n ≤ 100 2\le n\le 100 2n100
    1 ≤ a i ≤ 1 0 6 1\le a_i\le 10^6 1ai106
  • 【思路】赛内
    经过分类讨论 (wa过一遍),能得出比较简洁的结论:
    (1)如果 a b s ( a i − a i − 1 ) > 1 abs(a_i-a_{i-1})>1 abs(aiai1)>1,表示路是通的,花费为 0 0 0
    (2)如果每个障碍物的纵坐标都相同,那么花费为 min ⁡ ( 2 v , u + v ) \min(2v,u+v) min(2v,u+v)
    (3)否则,花费为 min ⁡ ( u , v ) \min(u,v) min(u,v)
    第一个结论易得。
    第二个结论,只能把其中一行水平方向移动两次,或者横一次竖一次即可。
    第三个结论,那么一定有一个障碍物是凸出来的,只用水平一次或者竖直一次即可。
  • 【代码】

C: Pekora and Trampoline | d p dp dp

  • 【题意】
    n n n 个蹦床,第 i i i 个的强度为 S i S_i Si
    小P 每轮可以自己选择一个位置开始蹦。
    如果目前位置的蹦床强度为 S S S,那么他蹦到的下一个位置为 i + S i+S i+S,然后 S S S 变成 max ⁡ ( 1 , S − 1 ) \max(1,S-1) max(1,S1)
    只有他的位置 > n >n >n ,那么他才能停下来,开始下一轮的蹦床。
    问你:最少几轮,才可以使得所有蹦床的强度都为 1 1 1
  • 【范围】
    1 ≤ n ≤ 5000 1\le n\le 5000 1n5000
    1 ≤ S i ≤ 1 0 9 1\le S_i\le 10^9 1Si109
  • 【思路】赛内
    容易得到,第一个强度非 1 1 1 的蹦床一定要从该位置开始蹦(当然也可以从前面开始蹦,但是前面的强度都是 1 1 1,于是和该位置开始蹦等价)。
    该位置需要至少蹦 S i − 1 S_i-1 Si1才能把该位置的强度变为 1 1 1
    但是由于 S i S_i Si 可以很大,大到 1 e 9 1e9 1e9,不可能一次一次去模拟蹦。我们想到了 d p dp dp
    d p [ i ] dp[i] dp[i] 表示蹦到该位置的次数。
    于是对于每一个位置,我们需要多蹦的次数为: max ⁡ ( 0 , S i − 1 − d p [ i ] ) \max(0,S_i-1-dp[i]) max(0,Si1dp[i])
    然后我们开始蹦。如果 S i = 3 S_i=3 Si=3,也就是我们要让 d p [ i + 3 ] + + , d p [ i + 2 ] + + dp[i+3]++,dp[i+2]++ dp[i+3]++,dp[i+2]++
    那么剩下的次数?当然是都给 d p [ i + 1 ] dp[i+1] dp[i+1] 了。
    还有一个注意点,就是如果 S i S_i Si 特别大,大多数都蹦到外面去了,我们和边界取小即可。
  • 【代码】
    时间复杂度: O ( n 2 ) O(n^2) O(n2)
    46 / 2000 M s 46/2000Ms 46/2000Ms
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
ll aa[MAX];
ll dp[MAX];
int main()
{
    int T = read();
    while(T--){
        int n = read();
        ll cnt = 0;
        for(int i = 0;i <= n;++i)dp[i] = 0;
        for(int i = 1;i <= n;++i){
            aa[i] = read_ll();
            cnt += max(0LL,aa[i] - dp[i] - 1);		/// 累加答案
            dp[i] = max(dp[i],aa[i] - 1);			/// 次数取大
            if(i + aa[i] > n){						/// 跳出边界的不要了
                int tmp = aa[i];
                aa[i] = n - i;
                dp[i] -= tmp - aa[i];
            }
            for(ll j = min((ll)n,aa[i] + i);j >= i + 2;--j){	/// 开始蹦
                dp[j]++;
                dp[i]--;
            }
            dp[i+1] += dp[i];						/// 剩下的都是蹦到下一个位置
        }
        printf("%lld\n",cnt);
    }
    return 0;
}

D:Zookeeper and The Infinite Zoo | 位运算

  • 【题意】
    有无穷个顶点,第 i i i 个点的值为 i i i
    u u u u + v u+v u+v 连一条有向边 当且仅当 u & v = v u\&v=v u&v=v
    q q q 个询问,每次问你 u i u_i ui 是否能通过有向边走到 v i v_i vi
  • 【范围】
    1 ≤ q ≤ 1 0 5 1\le q\le 10^5 1q105
    1 ≤ u i , v i < 2 0 30 1\le u_i,v_i<20^{30} 1ui,vi<2030
  • 【思路】赛内
    按照连边规则,我们容易想到直接打个表,但是 i i i 能走到的顶点集合有些比较复杂。
    比较容易得到的是:如果 v = u v=u v=u,那么 u u u 能连向 2 u 2u 2u,同理 2 u 2u 2u 能连向 4 u 4u 4u ,也就是 u u u 能走到 2 k ⋅ u 2^k\cdot u 2ku,其中 k ∈ N k\in \mathbb{N} kN
    但是有一些连通比较奇怪。比如 5 5 5 能走到 18 18 18,我们看一下他们的二进制表示:
    5 0010 1 b 9 0100 1 b 18 1001 0 b \begin{matrix} 5&00101_b\\ 9&01001_b\\ 18&10010_b \end{matrix} 591800101b01001b10010b
    我们选择 10 1 b 101_b 101b 的子集 10 0 b 100_b 100b,然后给累加其中,得到了 100 1 b 1001_b 1001b
    然后每一位都左移一格,得到了 1001 0 b 10010_b 10010b
    我们感觉,好像把一些 1 1 1 向左移动就是能走通的情况
    继续看这个例子:
    5 010 1 b 6 011 0 b 8 100 0 b \begin{matrix} 5&0101_b\\ 6&0110_b\\ 8&1000_b \end{matrix} 5680101b0110b1000b
    这里, 5 → 6 5\rightarrow6 56就是把末尾 1 1 1 向左移动一格的例子。
    对于 6 → 8 6\rightarrow8 68,我们选择 011 0 b 0110_b 0110b 的子集 1 0 b 10_b 10b,然后给累加其中,得到了 100 0 b 1000_b 1000b
    也就是说,如果有多个 1 1 1 在一起,我们左移的抉择是可以把多余的 1 1 1 给吞掉的。
    于是,题目就转变为: u u u 的二进制表示和 v v v 的二进制表示给定,能否只运用以上两条规则将 u u u 变成 v v v
    我们只要从右往左计算可用的 1 1 1 的数量。如果 u u u 的末 i i i 位为 1 1 1 ,那么可用 1 1 1 的数量增加。如果 v v v 的末 i i i 位为 1 1 1,那么可用 1 1 1 的数量减少。
    如果全部处理完,每一步可用 1 1 1 的数量非负,那么即是可以走通的。否则无法走通。
  • 【代码】
    时间复杂度: O ( 30 q ) O(30q) O(30q)
    61 / 3000 M s 61/3000Ms 61/3000Ms
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
int aa[MAX],bb[MAX];
int main()
{
    int T = read();
    while(T--){
        int s1 = 0,s2 = 0;
        ll a = read_ll();
        ll b = read_ll();
        if(a > b){
            puts("NO");
            continue;
        }
        if(a == b){
            puts("YES");
            continue;
        }
        for(int i = 1;i <= 30;++i){
            aa[i] = bb[i] = 0;
        }
        while(a){
            if(a&1)aa[++s1] = 1;
            else aa[++s1] = 0;
            a /= 2;
        }
        while(b){
            if(b&1)bb[++s2] = 1;
            else bb[++s2] = 0;
            b /= 2;
        }
        int shu = 0;
        bool can = true;

        for(int i = 1;i <= s2;++i){
            shu += aa[i];
            shu -= bb[i];
            if(shu < 0){
                can = false;
                break;
            }
        }
        puts(can ? "YES" : "NO");
    }
    return 0;
}

E:Fib-tree | 树 + 搜索

  • 【题意】
    F k F_k Fk 表示第 k k k 个斐波那契数。
    一颗树是斐波那契树,首先该数的节点个数是某个斐波那契数。其次,还得满足一下两个条件之一:
    (1)该树的节点个数为 1 1 1
    (2)通过割断一条边,把它分为两个子树,这两个子树都是斐波那契树。
    给你一棵树,问你该树是不是斐波那契树。
  • 【范围】
    1 ≤ n ≤ 2 ⋅ 1 0 5 1\le n\le 2\cdot 10^5 1n2105
  • 【思路】参考题解
    如果该树的节点个数为 F k F_k Fk,那么你割边的唯一合法要求是:割完后两边的子树节点个数为 F k − 1 F_{k-1} Fk1 F k − 2 F_{k-2} Fk2
    然后一直递归割下去,直到某次无法割或者节点个数为 1 1 1 返回即可。
    正确性可以参考 t u t o r i a l tutorial tutorial
    那么问题来了,就是代码怎么敲了。这里需要多次计算子树大小 s z sz sz ,如果按一般 d f s dfs dfs 的做法,去暂时保存 s z [ ] sz[] sz[] 数组,那么会超时…我这里使用 s z [ x ] [ c e ] sz[x][ce] sz[x][ce] 表示第 c e ce ce 次不同迭代时候的 x x x 的子树大小(包括自己)。
    我们目前的树的大小为 F k F_k Fk,我们搜索到某一个 x x x 的儿子节点 y y y 满足 ∣ y ∣ = F k − 1 |y|=F_{k-1} y=Fk1 ∣ y ∣ = F k − 2 |y|=F_{k-2} y=Fk2,那么我们就把 x → y x\rightarrow y xy 的边给割掉,然后两边分别递归。
  • 【代码】
    时间复杂度: O ( n log ⁡ φ n ) O(n\log_{\varphi} n) O(nlogφn)
    空间复杂度: O ( n log ⁡ φ n ) O(n\log_{\varphi} n) O(nlogφn)
    218 / 1000 M s 218/1000Ms 218/1000Ms
    56 / 256 M b 56/256Mb 56/256Mb
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
int fib[100];
vector<pair<int,int> >V[MAX];
bool del[MAX];
int sz[MAX][60];
int n;
bool dfs(int,int,int,int,int);
bool solve(int,int,int,int,int);
int cnt;
		/// 目前树大小 Fib(a) ,目前访问 x 节点,父亲为 f ,第 ce 次迭代
bool dfs(int a,int b,int c,int x,int f,int ce){
    sz[x][ce] = 1;
    bool can = false;
    for(auto it : V[x]){
        int y = it.first;
        int w = it.second;
        if(y == f || del[w])continue;	/// 如果边删掉了就别访问了
        can |= dfs(a,b,c,y,x,ce);
        if(del[w])return can;			/// 立即终止,表示这条边在之前的搜索中删掉了
        if(sz[y][ce] == fib[b]){		/// 找到了,继续搜索
            del[w] = true;
            can = true;
            can &= solve(b,b-1,b-2,y,ce);	/// 必须两个子树都符合,用逻辑与操作
            can &= solve(c,c-1,c-2,x,ce);
            return can;
        }
        if(sz[y][ce] == fib[c]){
            del[w] = true;
            can = true;
            can &= solve(b,b-1,b-2,x,ce);
            can &= solve(c,c-1,c-2,y,ce);
            return can;
        }
        sz[x][ce] += sz[y][ce];
        if(can)break;
    }
    return can;
}

bool solve(int a,int b,int c,int x,int ce){
    if(fib[a] == 1)return true;		/// 这个特判一下
    cnt++;							/// 迭代层数增加
    bool can = dfs(a,b,c,x,x,cnt);
    return can;
}

int main()
{
    n = read();
    for(int i = 1;i < n;++i){
        int ta,tb;ta = read();tb = read();
        V[ta].push_back(make_pair(tb,i));
        V[tb].push_back(make_pair(ta,i));
    }
    int st = 1;
    fib[0] = fib[1] = 1;
    while(fib[st] < n){
        fib[st+1] = fib[st] + fib[st-1];
        st++;
    }
    if(fib[st] != n)puts("NO");
    else{
        bool can = solve(st,st-1,st-2,1,0);
        puts(can ? "YES" : "NO");
    }
    return 0;
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值