Codeforces Round #623 (Div. 2, based on VK Cup 2019-2020

题目链接:https://codeforces.com/contest/1315

A - Dead Pixel

诚心诚意的送分题。

题意:给一个n*m的格子矩阵,和一个坏掉的格子,找一个最大的不包含坏掉的格子的矩阵。

void test_case() {
    int n, m, x, y;
    scanf("%d%d%d%d", &n, &m, &x, &y);
    int ans = max(x * m, (n - (x + 1)) * m);
    ans = max(ans, max(y * n, (m - (y + 1)) * n));
    printf("%d
", ans);
}

B - Homecoming

比A题还没意思。

C - Restoring Permutation

题意:构造一个字典序最小的2n个元素的排列,使得题目给出的数组b,满足 (b_i=min(a_{2i-1},a_{2i}))

题解:首先既然是排列,就要检查数组b的元素本身就不重复。而且数组b中不会出现2n。既然数组b是按最小值构造的,那么肯定把这个元素放在左边,也就是 (a_{2i-1}=b_i) 。然后右边那个数就贪心取第一个大于他的数,这样字典序最小,同时也节约了最小值意义下的资源。所以从左到右扫描,每个数选大于他的最小的还没被选的数搭档。好像也很没有意思,假如是要带log的做法,可以二分。假如是要接近常数的做法,可以用并查集实现的可删除链表,每个数存下一个可能没有被删除的数的位置。

D - Recommendations

题意:给n个数的数组x,以及n个数的数组t,把xi变为xi+1的消耗为ti,求最小的总消耗使得数组x中的元素各不相同。

题解:考虑一个暴力算法:首先,每次选择最小的xi,若xi无重复,则continue,否则,一直选择消耗ti最小的xi,把他变成xi+1,直到xi无重复。发现这个算法可以优化,就是每次依然是变成xi+1,但是只需要直接保留消耗ti最大的xi即可。

pii p[200005];
map<int, int> M;

void test_case() {
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &p[i].first);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &p[i].second);
    sort(p + 1, p + 1 + n);
    int curx = -1;
    ll sum = 0, ans = 0;
    for(int l = 1, r; l <= n; l = r + 1) {
        while(curx != -1 && curx != p[l].first) {
            auto it = M.rbegin();
            sum -= it->first;
            if(it->second == 1)
                M.erase(it->first);
            else
                it->second -= 1;
            ans += sum;
            curx += 1;
            if(M.empty())
                curx = -1;
        }
        curx = p[l].first;
        for(r = l; r + 1 <= n && p[r + 1].first == p[l].first; ++r);
        for(int i = l; i <= r; ++i) {
            M[p[i].second] += 1;
            sum += p[i].second;
        }
        auto it = M.rbegin();
        sum -= it->first;
        if(it->second == 1)
            M.erase(it->first);
        else
            it->second -= 1;
        ans += sum;
        curx += 1;
        if(M.empty())
            curx = -1;
    }
    while(curx != -1) {
        auto it = M.rbegin();
        sum -= it->first;
        if(it->second == 1)
            M.erase(it->first);
        else
            it->second -= 1;
        ans += sum;
        curx += 1;
        if(M.empty())
            curx = -1;
    }
    printf("%lld
", ans);
}

但是其实ti的范围很小,不需要用map这么复杂也可以,用权值线段树就行。

1D - Tourism

题意:给n(<=80)个点的边带权有向完全图,边权由邻接矩阵给出,选出一个从1开始的边权最小的k(<=10)条边的回路(不一定是简单路径,也就是可以重复经过同一个点,也可以重复经过同一条边),要求这个回路中,不存在奇数条边的环。保证k是偶数。

假做法:假如没有“不存在奇数条边的环”这个限制,整一个分层图就可以,复杂度也对(相邻两层之间有n^2条边,一共800个点6400条边)。加上这个限制的话,只需要一开始预处理出一次经过两条边的路径,然后把路径抽象成新的边,再建一次分层图即可。这个算法是错的,样例都可以检查出来1->5->2->1->5,存在了一个奇数环。仔细研究发现分层图的想法没有保留到达这个点的时候经过了哪些点,所以也不能从中限制下一条边的取值,这个算法是不可行的。

题解1:枚举这条长度为k的路径中,出现在偶数位置的点,这些点至多有k/2个,其中这些点除了最后一个一定是1以外,其他的点都不能是1。共有n^{k/2-1}种路径,然后要在这些点之间连接边数为2的路径。因为不能出现奇数条边的环,所以不能通过已经被枚举的点来松弛,所以保留k/2+1条最短的边数为2的路径,把通过被枚举的点松弛的路径跳过,就得到了正确的路径。最后记得补上1到枚举的第一个点的边的长度。

题解2:随机算法:给所有点随机黑白染色,变成一个二分图,规定和1同色的称为黑色,那么需要找一条“黑(1)-白-黑-...-黑(1)”的路径,因为同一个点必是同色,所以这样的路径不存在奇数条边的环,然后用分层图O(k*E)跑出最短路(同一层之间没有边的分层图,是不需要dijkstra的,因为同一层之间先更新哪个是不影响的,本质上是个简单的DAG上动态规划,dijkstra因为可以解决无负权边的带环图的情况,导致复杂度变高)。

最坏情况下,k=10,除了1以外最短路最多有9个点,只有这条路上的点被唯一正确染色时,才会把这条最短路统计进去,单次染色就成功的概率为1/512。由于n和k很小,可以进行10000次随机染色,10000次都失败的概率是(511/512)^10000=3e-9。

收获:

1、最短路算法的特点:
floyd:复杂度高,多源最短路,可以处理负权边的情况,可以发现图中是否有负环。
SPFA:复杂度一般比floyd稍低,单源最短路,可以处理负权边的情况,可以发现图中包含源点的负环。
dijkstra:复杂度低,单源最短路,不能处理负权边的情况。
DAG上DP:复杂度比dijkstra还低,但只能处理DAG。

2、和奇数偶数相关的图,有可能变成二分图来思考。

3、数据量小但是又很少人过的题目,一般都是比较巧妙的枚举或者状压。也有可能因为数据量小可以使用多次随机的算法蒙混过关。