Educational Codeforces Round #158 (Div. 2) A~E

A. Line Trip

题意:

你需要驾驶一辆车从 0 0 0开到 x x x,再从 x x x开回 0 0 0,同时 0 0 0 x x x之间有 n n n个加油站,每次到达加油站都可以加满油(出发时满油),且移动一个单位距离需要1升油,问油箱至少需要有多大?

分析:

把起点终点以及加油站视为一条线上的点,记录相邻两点距离的最大值即可。

Tips:最后一个加油站到终点再返回才能加油,需要记录的距离是往返的距离

代码:

#include<bits/stdc++.h>
using namespace std;

int a[100005];

void solve() {
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    int maxn = (k - a[n]) * 2;
    for (int i = 1; i <= n; i++) {
        maxn = max(maxn, a[i] - a[i - 1]);
    }
    cout << maxn << endl;
}
int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

B. Chip and Ribbon

题意:

开始时,你有一个包含 n n n个数的序列,第一个数为1,其他数全为0,你可以进行以下两种操作,问至少多少次操作2,可以使得序列与输入的序列一致:

  • 操作1:将序列中第i+1个数修改为第i个数

  • 操作2:选择一个位置,给这个位置上的数加一

分析:

由于操作1无需花费,那么能使用操作1时一定会选择操作1

将序列分成若干段,每段都是最大递减子串。

由于序列只能增加不能减少,因此当序列递减时,所需的操作2次数就是序列第一个数字减一(开始就有1)。

但是从第二个段起,每个段的部分都可以从前一段通过操作1获得。但是可以通过操作1获得的最大数为上一个段的最后一个元素,因此当前段的费用为当前段第一个数字减去上一个段中的最后一个数。

代码:

#include<bits/stdc++.h>
using namespace std;

int a[200005];

void solve() {
    int n;
    cin >> n;
    long long ans = -1;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        if (a[i] > a[i - 1]) {
            ans += a[i] - a[i - 1];
        }
    }
    cout << ans << endl;
}
int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

C. Add, Divide and Floor

题意:

给出一个序列 a 1 , a 2 , . . . , a n a_1, a_2, ..., a_n a1,a2,...,an,你每次可以选择一个数字 x x x,将序列中的所有数字变为 ⌊ a i + x 2 ⌋ \lfloor\frac{a_i + x}{2}\rfloor 2ai+x,问最少几次操作可以使得序列中的数字全部相同。

如果操作次数小于等于 n n n,你需要输出选择的所有 x x x

分析:

实际上只需要考虑序列中最大的和最小的数,当操作使最大的数和最小的数都相等后,其他数字也一定相等了。

然后看到操作 ⌊ a i + x 2 ⌋ \lfloor\frac{a_i + x}{2}\rfloor 2ai+x,可以想到,每次操作只能使序列中的数字向一个数的差减少一半,那么可以选择最小数或最大的数作为基准点(即选择的数 x x x),让其他数字向基准点靠近。

那么所需的操作次数为 ⌈ l o g ( 最大数-最小数 2 ) ⌉ \lceil log(\frac{\text{最大数-最小数}}{2}) \rceil log(2最大数-最小数)⌉,如果需要输出 x x x,选择均输出序列中最大或最小的数即可。

代码:

#include<bits/stdc++.h>
using namespace std;

int a[200005];

void solve() {
    int n;
    cin >> n;
    int maxn = -1, minn = 2e9;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        maxn = max(maxn, a[i]);
        minn = min(minn, a[i]);
    }
    int ans = 0;
    while (maxn != minn) {
        maxn = (maxn + minn) / 2;
        ans++;
    }
    cout << ans << endl;
    if (ans <= n){
        for (int i = 0; i < ans; i++) { 
            cout << maxn << ' ';
        }
    }
    if (ans) cout << endl;
}
int main() {
    int Case;
    cin >> Case;
    while (Case--) {
        solve();
    }
    return 0;
}

D. Yet Another Monster Fight

题意:

n n n只怪物,你只能使用一次闪电链对其中一个怪物造成伤害,问闪电链开始的威力需要为多少才能保证将这些怪物全部消灭。

闪电链的伤害规则:

  • 闪电链对每个怪物只能造成一次伤害

  • 闪电链每次造成的伤害都比上一次少一

  • 闪电链会随机在已伤害的怪物怪物相邻的未被伤害的怪物中随机挑选一个造成伤害

分析:

实际上,当你选择了一个怪物释放闪电链后,闪电链只能在已伤害的怪物左右选择一个怪物伤害,那么被攻击的怪物都是连续的,且每次只能在已被攻击的怪物区域左边或右边选择一个攻击。

由于闪电链的攻击是随机的,因此需要考虑最坏情况:

  • 第i个怪物被攻击时,最坏的情况就是当左边所有的怪物都被攻击或当右边所有的怪物都被攻击了,才轮到当前这个怪物被攻击。

因此需要使用两个数组,分别记录闪电链开始落在每个怪物左右的最坏情况,并使用动态规划思想将这两个数组维护为左右区间最少需要攻击力才能将怪物全部消灭。

维护完成后,枚举开始攻击的怪物,选择每个怪物所需的闪电链初始攻击力为以下三种情况中的最大值:

  • 消灭当前怪物所需的攻击力

  • 消灭所有左侧怪物所需的攻击力

  • 消灭所有右侧怪物所需的攻击力

记录枚举过程中的最小初始攻击力即可。

代码:

#include<bits/stdc++.h>
using namespace std;
long long n, a[300005], L[300005], R[300005];
void solve() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i <= n; i++) {
        L[i] = max(L[i - 1], a[i] + n - i);//L[i]记录的是消灭1~i上怪物所需的攻击力
    }
    for (int i = n; i >= 1; i--) {
        R[i] = max(R[i + 1], a[i] + i - 1);//R[i]记录的是消灭i~n上怪物所需的攻击力
    }
    long long ans = 2e9;
    for (int i = 1; i <= n; i++) {
        long long cost = a[i];//击败当前怪物的攻击力
        if (i > 1) {//左边还有怪物需要消灭
            cost = max(cost, L[i - 1]);
        }
        if (i < n) {//右边还有怪物需要消灭
            cost = max(cost, R[i + 1]);
        }
        ans = min(ans, cost);
    }
    cout << ans << endl;

}
int main(){
    solve();
    return 0;
}

E.Compressed Tree

题意:

给你一棵由 n n n 个节点组成的树。每个节点上都写有一个数字;节点 i i i上的数字等于 a i a_i ai
可以执行任意次数的以下操作(可能为零):选择一个最多有 1 1 1条边的节点,并将该节点从树中删除。可以删除所有节点。

完成所有操作后,准备压缩树。压缩过程如下。当树中有一个节点恰好有 2 2 2 条边时,执行以下操作:删除该节点,用一条边连接其邻居。

可以看出,如果在压缩过程中有多种选择删除节点的方法,那么得到的树还是一样的。

请你计算在任意次数的上述操作后,求出压缩完树以后的所有节点的权值之和最大。

分析:

对于一个节点来说,最终压缩完树以后有 4 种情况:

1、只保留了自己一个节点。

2、保留了自己和自己邻边上一个节点。

3、保留了邻边上的两个节点。

4、保留了自己和邻边上 2 2 2 个以上的节点。(这样在压缩的时候就不会把自己删了)

因此用 d p [ n ] [ 4 ] dp[n][4] dp[n][4]来分别表示这四种状态。接下来考虑如何从子节点上转移,若节点只有一个子节点,那么就只有 1、2 两种情况。如果节点有两个子节点,那么就会出现 1、2、3 三种情况。如果子节点大于 2 个的话,那么就需要对子节点的值进行排序了,肯定是越大的越好。对于情况 4,并不是所有的子节点都需要选择,若子节点的值小于 0,那么就代表这子节点是无需保留的,删除即可。

接下来考虑子树的值如何选择:对于情况 1 和情况 4,直接继承。对于情况 2,在压缩的过程中会把子树结点给压缩掉,所以需要减去子节点的值。对于情况 3,原本是不保留子节点的,但是由于需要连到父亲上,所以子节点需要保留,因此需要增加子节点的值。因此一个子节点的值即为: m a x ( d p [ t m p ] [ 1 ] , d p [ t m p ] [ 2 ] − a [ t m p ] , d p [ t m p ] [ 3 ] + a [ t m p ] , d p [ t m p ] [ 4 ] ) max(dp[tmp][1],dp[tmp][2]-a[tmp],dp[tmp][3]+a[tmp],dp[tmp][4]) max(dp[tmp][1],dp[tmp][2]a[tmp],dp[tmp][3]+a[tmp],dp[tmp][4]);

接下来走任意一点开始走一遍 d f s dfs dfs,时刻记录最大值。(只有一条链或者两个点的情况下特殊处理一下即可)

hint: 本题输入输出较多,建议使用scanf/printf进行输入输出

代码:

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;
#define endl '\n'
const ll N = 5e5 + 10;
const ll mod = 1e9 + 7;
const ll inf = 1e18;

ll gcd(ll a, ll b) {
    return b > 0 ? gcd(b, a % b) : a;
}

ll lcm(ll a, ll b) {
    return a / gcd(a, b) * b;
}

ll n, m;
ll a[N];
ll deg[N];
vector<ll> g[N];
ll dp[N][4];

void init(ll n) {
    for (ll i = 0; i <= n; i++) {
        a[i] = 0, deg[i] = 0, g[i].clear();
    }
}

ll cmp(ll a, ll b) {
    return a > b;
}

ll ans = 0;

void dfs(ll x, ll fa) {
    dp[x][0] = a[x], dp[x][1] = -inf, dp[x][2] = -inf, dp[x][3] = -inf;
    vector<ll> tmp;
    for (auto v: g[x]) {
        if (v == fa)
            continue;
        dfs(v, x);
        tmp.push_back(max(dp[v][0], max(dp[v][1] - a[v], max(dp[v][2] + a[v], dp[v][3]))));
    }
    sort(tmp.begin(), tmp.end(), cmp);
    if (tmp.size() >= 1) {
        dp[x][1] = a[x] + tmp[0];
    }
    if (tmp.size() >= 2) {
        dp[x][2] = tmp[0] + tmp[1];
    }
    if (tmp.size() >= 3) {
        dp[x][3] = a[x] + tmp[0] + tmp[1] + tmp[2];
        for (ll i = 3; i < (ll) tmp.size(); i++) {
            if (tmp[i] < 0) {
                break;
            }
            dp[x][3] += tmp[i];
        }
    }
    ans = max(ans, max(dp[x][0], max(dp[x][1], max(dp[x][2], dp[x][3]))));
}

void solve() {
    cin >> n;
    for (ll i = 1; i <= n; i++)
        cin >> a[i];
    ll max_deg = 1;
    for (ll i = 1; i < n; i++) {
        ll x, y;
        cin >> x >> y;
        g[x].push_back(y);
        g[y].push_back(x);
        deg[x]++;
        deg[y]++;
        max_deg = max(max_deg, max(deg[x], deg[y]));
    }
    if (max_deg == 1) {
        if (a[1] < 0) {
            cout << max((ll) 0, a[2]) << endl;
        } else {
            cout << max(a[1], a[1] + a[2]) << endl;
        }
    } else if (max_deg == 2) {
        sort(a + 1, a + 1 + n, cmp);
        if (a[1] < 0) {
            cout << max((ll) 0, a[2]) << endl;
        } else {
            cout << max(a[1], a[1] + a[2]) << endl;
        }
    } else {
        ans = 0;
        dfs(1, 0);
        cout << ans << endl;
    }
    init(n);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr); cout.tie(nullptr);
    int t = 1;
    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}

以下学习交流QQ群,群号: 546235402,大家可以加群一起交流做题思路,分享做题技巧,欢迎大家的加入。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值