【解题报告】Codeforces Round #303 (Div. 2)

题目链接


A.Toy Cars(Codeforces 545A)

思路

简单实现题。将表示碰撞结果的矩阵保存下来,然后检查每个车辆是否是“good car“即可。

代码

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

const int maxn = 110;
bool good;
int n, G[maxn][maxn];
vector <int> v;

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            scanf("%d", &G[i][j]);
        }
        good = true;
        for(int j = 1; j <= n; j++) {
            if(i != j && G[i][j] % 2) {
                good = false;
            }
        }
        if(good) v.push_back(i);
    }
    printf("%d\n", v.size());
    for(int i = 0; i < v.size(); i++) {
        printf("%d ", v[i]);
    }
    puts("");
    return 0;
}

B.Equidistant String(Codeforces 545B)

思路

要构造一个与两个输入字符串的汉明距离都相等的字符串,只要从一个字符串出发“走过”两个字符串汉明距离的一半就可以了。具体地,假设输入的串为 ,我们枚举 i ,当 A[i] B[i] 不相同的时候记录下 i (放在数组 v 中)。当我们记录下的 i 的个数 cnt 为奇数时肯定不能构造出我们的目标字符串。当 cnt 为偶数的时候,我们挑选出 v 中的一半的元素,并将与这些元素对应的 A 上的字符修改掉(例如,原来是 1 ,修改后就变成 0 )。修改后的字符串就是我们需要的目标串了。

代码


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

const int maxn = 1e5 + 5;
char s[maxn], t[maxn];
int n, m, cnt;
vector <int> v;

int main() {
    scanf("%s%s", s, t);
    cnt = 0;
    n = strlen(s);
    for(int i = 0; i < n; i++) {
        if(s[i] != t[i]) {
            v.push_back(i);
            cnt++;
        }
    }
    if(cnt % 2) puts("impossible");
    else {
        m = cnt >> 1;
        for(int i = 0; i < m; i++) {
            s[v[i]] = t[v[i]];
        }
        printf("%s\n", s);
    }
    return 0;
}

C.Woodcutters(Codeforces 545C)

大意

在一条数轴上有若干棵树。每棵树有一个高度,问最多能砍多少棵树,使得没有倒下的树碰到其它的树。

思路

这题最直观的解法是搜索。搜索树的每一层都表示一次决策,即该层表示的树是往左倒还是往右倒。但由于 n 的规模太大,这个算法是不可行的。于是尝试用记忆化搜索或动态规划去优化它。假设我们考虑到第 i 棵树,并且在前 i 棵树中,最多可以放倒 d 棵树。我们发现, d 的取值还依赖最后一棵树的状态,于是我们需要记录 d[i][j] ,表示前 i 棵树在第 i 棵树的状态是j的时候能倒下的最多的树的数量。然后分三种情况讨论:

  • 第i棵树直立的时候。不论第 i 棵想要直立是可以“不看前一棵树的脸色”的,因此有 d[i][0]=max{d[i1][j]0j2}
  • 第i棵树向左倒的时候。第i棵树想要向左倒,就必须“看前一棵树的脸色”,也就是看前一棵树给它留下了多少空位。当前一棵树没有向右倒的时候有 d[i][1]=max(d[i1][0],d[i1][1])+1 ,当前一棵树向右倒的时候有 d[i][1]=max(d[i][1],d[i1][2]+1) 。在实现的时候要注意判断位置是否足够让一棵树倒下。
  • 第i棵树向右倒的时候。这种情况的最优值一定比第 i 棵树直立的时候要多 1 ,因此 d[i][2]=d[i][0]+1

最后的答案就是 d[n][2]

代码

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

const int maxn = 1e5 + 10, INF = 1e9;
int n, ans, x[maxn], h[maxn], d[maxn][3];

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%d%d", &x[i], &h[i]);
    }
    // 设置两棵虚拟的树 
    x[0] = - INF;
    x[n+1] = INT_MAX;
    for(int i = 1; i <= n; i++) {
        // 直立 
        d[i][0] = max(max(d[i-1][0], d[i-1][1]), d[i-1][2]);
        // 向左倒
        if(h[i] < x[i] - x[i-1]) {
            d[i][1] = max(d[i-1][0], d[i-1][1]) + 1;
        }
        if(h[i] + h[i-1] < x[i] - x[i-1]) {
            d[i][1] = max(d[i][1], d[i-1][2] + 1);
        }
        // 向右倒
        if(h[i] < x[i+1] - x[i]) {
            d[i][2] = d[i][0] + 1;
        }
    }
    printf("%d\n", d[n][2]);
    return 0;
}

思路

其实本题也不是没有贪心策略。对于两端的树而言,左端的树往左倒,右端的树往右倒一定是最优的策略。对于其它的树(从左向右扫描的话),先考虑向左倒再考虑向右倒一定是最优策略。(证明方法暂时没想到)

代码

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

const int maxn = 1e5 + 10;
int n, ans, x[maxn], h[maxn];

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%d%d", &x[i], &h[i]);
    }
    ans = 2;
    for(int i = 2; i < n; i++) {
        if(x[i] - x[i-1] > h[i]) {
            ans++;
        }
        else if(x[i+1] - x[i] > h[i]) {
            x[i] += h[i];
            ans++;
        }
    }
    printf("%d\n", n <= 2 ? n : ans);
    return 0;
}

D.Queue(Codeforces 545D)

大意

有若干个人在排队等待服务,服务员服务每个人的时间都是不同的,对于一个人而言,若他前面的人的被服务总时间大于它自己被服务的时间,他就会感到失望。问最多可以让多少人感到不失望。

思路

拿到这道题,直觉告诉我应该是先对数据排序(因为感性认知告诉我忍耐值小的人放在前面似乎能够满足尽可能多的人)然后再微调(感性认知不一定是事实)。为了方便讨论我们用O表示排好序的队列中不会失望的某个人,用X表示排好序的队列中会失望的某个人。在微调的过程中,首先 OOO...O 形式的情况下,显然不应该调整任何人的顺序。其次在 OOO...OX 形式的情况下, X 不应该向前调整,应为这样是没有任何好处的(为了满足一个人而让至少一个人失望)。相反地, X 应该向后调整。既然它必须向后调整,反正它无论如何也得无法不失望了,不如就让他站到队列末尾。这样以来,在他身后的原本所有可能是 O 的人仍然是 O ,原本可能是 X 的人有可能因为他的离开变成 O
所以我们总结出算法:先对输入数据排序,然后扫描排序后数据的同时维护前缀和 sum 和不会失望的人数 cnt 。扫描经过的每个 O 的值都被加入 sum 中,而 X 因为被加到队列末尾所以它的值不能加入到 sum 中。这样,在扫描的过程中我们根据 sum 判断某个人是否是 O ,然后更新 sum cnt 。最终的 cnt 就是我们要的答案。

代码

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

const int maxn = 1e5 + 10;
int n, cnt, sum, a[maxn];

int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
    }
    sort(a, a + n);
    cnt = sum = 0;
    for(int i = 0; i < n; i++) {
        if(a[i] >= sum) {
            sum += a[i];
            cnt++;
        }
    }
    printf("%d\n", cnt);
    return 0;
}

E.Paths and Trees(Codefoeces 545E)

大意

在一个图 G 中找一棵“从 u 出发的最短路树 T ”,最短路树的性质是: T G 的生成树且 u 到树上任意点 v 的最短距离等于 u v 在原图中的最短距离。

思路

这个问题同时具有最小生成树和最短路径的特点,但最小生成树无法算出起点到每个点的最短路径,因此只考虑用后者来解决问题。
假设我们已经在原图上跑了一遍 Dijkstra 算法。那么我们我们就有了 d 数组记录下起点到每个点的最短路径,我们甚至还能以用 f 数组记录前驱边的方式还原整个路径。然后观察任意一个非起点的结点 u ,从起点至 u 的最短路径一定包含在题目要求我们找的那棵树 T 中。这样看来似乎只要将起点到所有点的最短路径经过的边加入到答案集合中就行了。但是如果同时有多条从起点至 u 最短路径呢?那么我们就选择 u 的前驱边中最短的那条边所在的最短路径。例如,假设现在有三个点 a,b,c 。其中 b,c a 的前驱点(意思是起点到 a 的两条长度相同的最短路径分别经过 b c )。现在从起点至 b ,从起点至 c 的两条最短路径已经被加入到树 T 中。那么我们为了让树包含 a 实际上在 T 加入 ba 边和 ca 边都是可以的,但是若两者的权值分别为 3 5 的话,我们必然会选择权值更小的 ba
最后,在算法的实现上,我们需要跑一遍修改后的 Dijkstra 算法,让该算法支持记录权值最小的前驱边。然后再遍历一遍图中的点就能够求出答案了。

代码

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

typedef long long ll;
// 优先队列用的结点
typedef pair <ll, int> p;
const int maxn = 3e5 + 10;
const ll INF = 1e15;
int n, m, u, v;
ll w, sum;
vector <int> ans;

// 存储边的信息的结构体
struct edge {
    int from, to;
    ll dist;
    edge(int from, int to, ll dist): from(from), to(to), dist(dist) {}
};

// Dijkstra算法模板
struct Dijkstra {
    // 保存最短路径距离
    ll d[maxn];
    // 保存点的前驱边
    int f[maxn];
    // 保存边集
    vector <edge> edges;
    // 邻接表
    vector <int> G[maxn];
    // 将一条边加入邻接表
    void addEdge(int u, int v, ll w) {
        G[u].push_back(edges.size());
        edges.push_back(edge(u, v, w));
    }
    void solve(int s) {
        // 初始化最短路径距离数组
        fill(d + 1, d + n + 1, INF);
        d[s] = 0;
        // 初始化优先队列
        priority_queue < p, vector<p>, greater<p> > pq;
        pq.push(p(0, s));
        while(!pq.empty()) {
            p node = pq.top();
            pq.pop();
            int u = node.second;
            if(node.first > d[u]) {
                continue;
            }
            for(int i = 0; i < G[u].size(); i++) {
                edge& e = edges[G[u][i]];
                int v = e.to;
                // 松弛的同时更新前驱边
                if(d[v] > d[u] + e.dist) {
                    d[v] = d[u] + e.dist;
                    f[v] = G[u][i];
                    pq.push(p(d[v], v));
                }
                // 修改前驱边
                if(d[v] == d[u] + e.dist && e.dist < edges[f[v]].dist) {
                    f[v] = G[u][i];
                }
            }
        }
    }
}o;

int main() {
    scanf("%d%d", &n, &m);
    while(m--) {
        scanf("%d%d%I64d", &u, &v, &w);
        o.addEdge(u, v, w);
        o.addEdge(v, u, w);
    }
    scanf("%d", &u);
    o.solve(u);
    sum = 0;
    for(int i = 1; i <= n; i++) {
        if(i == u) {
            continue;
        }
        sum += o.edges[o.f[i]].dist;
        ans.push_back(o.f[i] / 2 + 1);
    }
    printf("%I64d\n", sum);
    for(int i = 0; i < ans.size(); i++) {
        printf("%d ", ans[i]);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值