Codeforces Round #787 (Div. 3)(A-F部分题解)

文章详细介绍了CodeforcesRound#787(Div.3)中的六道编程题目,包括题目要求、解题思路和通过评测的代码。涉及的算法包括模拟、贪心策略、并查集和bfs搜索。
摘要由CSDN通过智能技术生成

原题链接:Codeforces Round #787 (Div. 3)

A. Food for Animals(签到)


题目大意:

x x x 只狗,和 y y y 只猫。你可以购买 a a a 包狗粮, b b b 包猫粮, c c c 包通用粮(猫和狗都能吃)。问你在购买之后,是否能正好分完粮,或者不够分。


解题思路:

判断还有剩下几只没分到的,看看通用粮够不够分即可。


AC代码:
using i64 = long long;
void solve()
{
    i64 a, b, c, x, y;
    cin >> a >> b >> c >> x >> y;
    if (a - x < 0) c += a - x;
    if (b - y < 0) c += b - y;
    if (c >= 0) YES;
    else NO;
}

B. Make It Increasing(模拟)


题目大意:

给出一个数列 a a a ,你可以任意选择一个 a i a_i ai 替换成 ⌊ a i 2 ⌋ \lfloor \frac{a_i}{2} \rfloor 2ai 问最少操作多少次,才能让 a a a 变成一个严格递减的数列,如果不能达到,输出 − 1 -1 1


解题思路:

看一眼范围, 1 ≤ n ≤ 30 1\leq n \leq 30 1n30,直接暴力模拟改即可。
要判断是不是 − 1 -1 1 ,则只需要看 a a a 数组最前面两个数一直向下取整后是否都是 0 0 0 即可(易知如果存在两个 0 0 0 则已经不是严格递减了)。


AC代码:
using i64 = long long;
 
void solve()
{
    int n; cin >> n;
    vector<int> arr(n);
    for (auto& x : arr) cin >> x;
    
    int ans = 0, cnt = 0;
    bool ok = true;
    while (true)
    {
        ok = true;//判断当前是否修改了
        for (int i = 0; i + 1 < n; ++i)
            if (arr[i + 1] <= arr[i]) //后一个小于自己 就除二
                arr[i] >>= 1, ok = false, ++ans;
        //不修改 或者无解就break
        if (ok || (arr[0] == 0 && arr[1] == 0)) break;
    }
    cout << ((arr[0] == 0 && arr[1] == 0) ? -1 : ans) << '\n';
}


C. Detective Task(思维)


题目大意:

房里有一幅画,有 n n n 个人按顺序进入房间,最后你发现房间里的画不见了。

现在你按顺序审问这 n n n 个人。按原来的顺序来回答你的问题: “ 1 1 1” 表示画还在,“ 0 0 0” 表示画不在了,“ ? ? ?” 表示自己没注意。

只有小偷会撒谎,其他人都是诚实的。需要你求出有几个人可能是小偷。


解题思路:

分析,题目给出的原序列一定是 111111000000 111111000000 111111000000 这样的序列。然后再参杂进 “ ? ? ?” 。

只有小偷撒谎了,这一段序列中,其他人都一定诚实,那么可以得到结论:

  • 对最后一个说 1 1 1 的人来说,前面的人一定都是诚实的,自己不一定诚实。
  • 对第一个说 0 0 0 的人来说,后面的人一定都是诚实的,自己不一定诚实。
  • 如果区间中间存在 ? ? ? ,我们并不能判断他是 1 1 1 还是 0 0 0 ,因此我们也要纳入怀疑名单中。
  • 特别地,只存在 1 1 1 ,说明最后一个人是小偷,只存在 0 0 0, 说明第一个人是小偷。

所以序列是 1 ? ? 1 ? 1 ? 1 ? ? ? 0 ? 000 ? ? 0 1??1?1?1???0?000??0 1??1?1?1???0?000??0 时候。
答案区间为 1 ? ? 1 ? 1 ? (    1 ? ? ? 0    ) ? 000 ? ? 0 1??1?1? (~~1???0~~) ?000??0 1??1?1?(  1???0  )?000??0

那么只要找到这两个 1 1 1 0 0 0 的下标,相减一下就可以了。
不得不说,这题思维难度还是很大的


AC代码:
void solve()
{
    string str;
    cin >> str;
    
    int n = str.size();
    int l = 0, r = n - 1;
    
    for (int i = 0; i < n; ++i)//找最后一个1
        if (str[i] == '1') l = i;
        
    for (int i = l; i < n; ++i)//找第一个0
        if (str[i] == '0') 
        { 
        	r = i; 
        	break; 
        }
        
    cout << r - l + 1 << '\n';
}


D. Vertical Paths(bfs)


题目大意:

给定一颗含有 n n n 个节点, n − 1 n - 1 n1 条边的树,问最少分成多少个从上到下的链,并输出这些链。


解题思路:

分成最少链看似困难,但是只要保证每条链包含节点最多,也就是每一条都能够尽量长,就可以满足最少的链了。

我们只要从根开始,每条边都建立从自己到父亲的单向边,从根进行 b f s bfs bfs ,得到每个节点的前驱,就可以得到回溯时的路径了。

考虑如何回溯,根据我们的建图,易知入度为 0 0 0 的点就是叶子节点,把所有入度为 0 0 0 的节点都进行一次回溯。

从叶子节点向上回溯,如果遇到之前走过的链就结束,记录这一条链所有遇到过的节点,把经过的点都标记一下。下一次回溯遇到被标记过的点就直接 break 就行了。

似乎很绕?看看代码配合解释就明白了


AC代码:
const int N = 2e5 + 10;
vector<int> g[N];//vector存图
vector<int> res;
bool vis[N];

void dfs(int u)
{
    vis[u] = true;
    for (auto& x : g[u])
        if (!vis[x]) dfs(x);
    res.emplace_back(u);//把路径上的点都存起来
}

void solve()
{
	fill(vis + 1, vis + n + 1, false);//多测清空
    for (int i = 1; i <= n; ++i) g[i].clear();

    int n, root; cin >> n;
    vector<int> in(n + 1);

    for (int i = 1, x; i <= n; ++i)
    {
        cin >> x;
        if (x == i) root = x;//判断根
        else g[i].emplace_back(x), ++in[x];//建单向边 记录入度
    }

    vector<vector<int>> ans;//ans保存每一条路径
    for (int i = 1; i <= n; ++i)
    {
        if (in[i]) continue;//如果不是是叶子节点 continue
        dfs(i), ans.emplace_back(res);
        res.clear();
    }

    cout << ans.size() << '\n';
    for (auto& rec : ans)//输出所有路径
    {
        cout << rec.size() << '\n';
        for (auto& x : rec) cout << x << ' ';
        cout << '\n';
    }
    cout << '\n';
}

E. Replace With the Previous, Minimize(贪心,并查集)


题目大意:

给出一个长度为 n n n 的字符串 s s s ,每次可以选择 s s s 中的一个字母,将所有该字母变成其之前的字母(例如: d → c d \rightarrow c dc c → b c \rightarrow b cb z → y z \rightarrow y zy 特殊地 a → z a \rightarrow z az )。

你只能操作不超过 k k k 次,使得这个字符串字典序最小。输出改变后的字符串 s s s


解题思路:

字典序最小的题一般都直接想到贪心。要字典序最小,那么前面的字母能向 a a a 靠近,就向 a a a 靠近。而且注意到,操作是对当前所有字母都有效的。

对于: s = f e d c b a s =fedcba s=fedcba k = 3 k = 3 k=3 来说,我们操作的序列就是这样的:

  • 第一次: s = e e d c b a s = eedcba s=eedcba
  • 第二次: s = d d d c b a s = dddcba s=dddcba
  • 第三次: s = c c c c b a s = ccccba s=ccccba

那么我们维护一下每个字母都改变成了哪一个字母就好了,这里可以用并查集。

字母每向前移动一次,就把他的父亲变成前一个节点,并且易知( a → z a \rightarrow z az)这个操作肯定不优,因此我们只需要考虑变到 a a a 这个字母停止就行了。

贪心地从 s s s 的第一个字母开始变,如果变到了 a a a 就从第二个字母开始变,以此类推。


AC代码:

int fa[26];//存父亲

int find(int x)
{
    while (x != fa[x]) x = fa[x] = fa[fa[x]];
    return x;
}

void solve()
{
    int n, k; cin >> n >> k;
    string str; cin >> str;

    //初始化并查集 0 为 a,1 为 b,以此类推
    iota(fa, fa + 26, 0);

    for (int i = 0; i < n; ++i)
    {
        int x = find(str[i] - 'a');//找到现在这个字母改变成了什么
        if (fa[x] != 0)//当前这个字母没有变到字母 a 则变换
        {
            while (k && fa[find(x)] != 0)
                k--, x = fa[x] = fa[x - 1];//每次变换 操作 k 都减少 1
        }
    }

    for (auto& x : str) 
        cout << char(find(x - 'a') + 'a');//将每个字母转换后输出
    cout << '\n';
}


F. Vlad and Unfinished Business(bfs)


题目大意:

给出一棵有 n n n 个节点, n − 1 n - 1 n1 条边的树,给出你根 x x x 和终点 y y y

你要去到 a 1 , a 2 , . . . , a k a_1,a_2,...,a_k a1,a2,...,ak 点之后,才能在终点 y y y 停下。每一个点可以访问多次,问你要达到上述条件,你要走的路程最小是多少。


解题思路:

通过手摸样例可以知道,我们不管先到达哪一个点 a i a_i ai,然后再折返去到另一个点 a j a_j aj,在最后返回到 y y y的时候,我们的最短路程都是固定的(前提是你走的是两点间的最短路,而且没重复经过 x → y x \rightarrow y xy 的最短路径)。

给出一个例子,比如我们现在的图是这样的:

那么我们要保证不重复经过 x → y x \rightarrow y xy 的最短路径,来达到目的的话,我们的路径应该是这样的:


观察得到,除了 x → y x \rightarrow y xy 的最短路径,其他所有的边经过的次数都是 2 2 2,因此我们只需要算出别的边一共有多少条,乘上 2 2 2 再加上 x → y x \rightarrow y xy 的最短路径的长度,即可得正确答案(如果某个点在 x → y x \rightarrow y xy 的最短路径上,那就不纳入考虑)。

我们执行一次 b f s bfs bfs,跑出每个点的前驱是谁,再回溯一下路径,如果当前点已经被回溯过就 break 掉。

(具体实现看代码就行了)


AC代码:

using i64 = long long;
const int N = 2e5 + 10;
vector<int> g[N];//邻接表存图
int pre[N], n, k, st, ed;//pre存前驱
bool vis[N];

inline void bfs()
{
    fill(vis, vis + 1 + n, false);//初始化访问数组
    //bfs板子
    queue<int> que;
    que.push(st);
    vis[st] = true;
    while (que.size())
    {
        auto t = que.front();
        que.pop();
        for (auto& x : g[t])
        {
            if (vis[x]) continue;
            pre[x] = t;
            vis[x] = true;
            que.push(x);
        }
    }
}

i64 mov(i64 u, i64 sum)//回溯路径
{
    while (vis[u])//如果当前点没被回溯到过
        vis[u] = false, u = pre[u], ++sum;
        //每次把vis置成false 防止重复经过一条边
    return sum;
}

void solve()
{
    cin >> n >> k;
    cin >> st >> ed;//st是起点 ed是终点
    
    for (int i = 1; i <= n; ++i) g[i].clear();//多测清空

    vector<int> ned(k);
    for (auto& x : ned) cin >> x;
    for (int i = 0, x, y; i < n - 1; ++i)
        cin >> x >> y, 
        g[x].emplace_back(y), 
        g[y].emplace_back(x);//建双向边
        
    i64 ans = 0;

    bfs();

    vis[st] = false;//起点已经去过了
    ans += mov(ed, 0);//加上x -> y的路径长度
    for (auto& x : ned)
        ans += mov(x, 0) << 1;//其他点的路径长度 * 2
    cout << ans << '\n';
}


G太难辣!!以后补了再补一下题解!!

谢谢观看!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值