【2019】0802 的 C 组模拟赛总结 + 题解

· · 个人记录

比赛状况

比赛地址

排名 / 分数:75 / 51.1

各题分数:11.1 / 40 / 0 / 0

我,今日,爆惨 ,打钱

今天这次比赛,我的状态真的是差 “飞” 了 —— 做题策略上的重大失误都不值得一提了,因为今天状态已经差到一个 80分(其实跟正解区别不大了) 的程序 没 交 上 去 …… 别的题也是很惨,水题 T1 和 T3 大脑宕机,同样很水的 T2 本来写了个 80 分的程序,结果又补上了个 40 分的 “优化” ……

而且,今天题目也很水 —— 两个暴力,两个接近模板的题目,理解起来十分容易。但是,连这么水的题目都能这么惨,我已经不知道接下来的比赛会有多惨了 ……

不过不能沮丧啊,虽然收获可能会很少,但还是要努力做呢。

听课总结

思路类

习惯类

重边、自环;

被分成不连通的若干块; (T4 不能直接用边数判断是否不连通的原因)

某种结构的退化;

负权、负环、负数结果;

解题报告

T1 disease (细菌)

题目地址

题意概括 。有 n 个列表,每个列表会有若干个元素,一共有 d 种元素。现在要从这些列表中选出若干个列表,使得选出的列表中元素 的种类 不超过 k 种,问你能选出的最多列表数。

**分析** 。这题就是暴力枚举,而且还没什么技巧 —— 因为 $d$ 很小,所以枚举选择 “元素” 的方案就可以了,最多不过 $2^{15}=32768$ 种可能,判断也顶多 $nd = 15000$ 种可能,去掉一些不合法(超过 $k$ 个元素)的方案完全不担心超时,就不多说什么了。 **代码** 。 ```cpp #include <cstdio> const int MAXD = 15 + 5; const int MAXN = 1000 + 5; int n, m, k, ans; struct cow { int tot; int have[MAXD]; } a[MAXN]; struct flag { int cnt; bool used[MAXD]; } b; void getNumber() { int t = 0; for (int i = 1; i <= n; ++i) { bool isSelect = true; for (int j = 1; j <= a[i].tot; ++j) if (!b.used[ a[i].have[j] ]) { isSelect = false; break; } if (isSelect) ++t; } if (t > ans) ans = t; } void getPlan(int i) { if (i > m) { getNumber(); return; } //Select if (b.cnt < k) { b.cnt++; b.used[i] = true; getPlan(i + 1); b.used[i] = false; b.cnt--; } //Unselect if (b.cnt + m - i >= k) getPlan(i + 1); } int main() { scanf("%d%d%d", &n, &m, &k); for (int i = 1; i <= n; ++i) { scanf("%d", &a[i].tot); for (int j = 1; j <= a[i].tot; ++j) scanf("%d", &a[i].have[j]); } getPlan(1); printf("%d\n", ans); return 0; } ``` ### $T2$ jumpcow (牛跳) [题目地址](http://120.77.82.93/junior/#contest/show/1649/1) **题意概括** 。现在有一个长度为 $P$ 的序列正整数序列,现在你需要从里面选数,使结果最大。 你必须按照原来的次序选择,但可以跳过某些数。而且,在你奇数次选择时,结果会加上当前这个数;在你偶数次选择时,结果会减去当前这个数。 初始值为 0,首次选择记为第 1 次选择,每个数只能选择一次。 $1 \leq P \leq 150000$ ,保证序列中的每个数范围为 $[1,300]$ 。 **分析** 。本题一眼看上去就知道 “不是贪心,是动态规划” ,而且状态和状态转移方程也十分显然 —— 因为 “加数” 和 “减数” 是交替出现的,所以将当前是 “加” 还是 “减” 也进行记录即可。所以,得到了下面的设计: 设 $f[i,0/1]$ 代表选第 $i$ 个数,并且是第 $j$ 次选择,余数为 $0/1$ 时的最大结果,有状态转移方程 $f[i,0/1] = max_{1 \leq j<i} (f[j,1/0]) + a[i]$ ,其中 $a[i]$ 是第 $i$ 个数的值。 因此,这个程序就得到了正确的 $O(p^2)$ 解法。 等会!这个时间复杂度绝对是很高的,不可能通过,怎么优化呢?观察这个式子,除了必要的 $O(p)$ 次枚举,时间都消耗在 “寻找最大值” 这个过程上了。而显然,前面的 $f[j]$ 不会变,每一次得出 $f[i]$ 后,也只会为我们的最大值添加一种可能,所以我们完全可以边做边记录 $f[0/1]$ 各自的最大值,就这样解决了问题。 **代码** 。由于本题过于简单,所以我试着压了一下行,结果可以 20 行 AC …… 虽然有点丑,但如果理解了上述过程,阅读起来应该也没什么压力。 ```cpp #include <cstdio> #include <algorithm> const int MAXN = 150000 + 5; int n, ans, max0, max1, f[MAXN][2]; int main() { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &f[i][1]); f[i][0] = -f[i][1]; ans = std::max(ans, std::max(f[i][0] += max1, f[i][1] += max0)); //表达式本身也有值 max0 = std::max(max0, f[i][0]); max1 = std::max(max1, f[i][1]); } printf("%d\n", ans); return 0; } ``` ### $T3$ water (水流) [题目地址](http://120.77.82.93/junior/#contest/show/1649/2) **题意概括** 。有一个 $n * m$ 大小的矩阵,矩阵中的每一个格子用小写字母 $[a, z]$ 表示高度,ASCII 码越大高度越高。 每个矩阵中有水,水可以上、下、左、右流向旁边的格子,并且只在旁边的格子高度小于等于当前格子高度时才能流动。 现在你要设置一些泵,这些泵的抽水能力是无穷的,但你必须合理设置泵,从而用最少的泵抽完所有的水,问满足抽完所有水时,最少的泵个数。 $1 \leq n,m \leq 50

分析 。显然,水会一直流动,流到最低点才会停止,因此所有的水都会汇聚到最低点,在最低点设置泵就可以了。但是,根据样例可以看出,一个地区可能会有很多个 “块”,“块” 的外围是 “一睹围墙” ,高度很高(至少比围墙外那一圈地区要高),使得里面的水没法流出去,因此泵就可能会设置多个。

所以,只要解决 “怎么找到最低点” 和 “怎么找到多个块” 这两个问题,题目就基本解决了。实际上,看到题目那么小的数据范围,我们很容易想到一个又好写、又能 “一箭双雕” 的方法 —— 每次找一个未标记的最低点,并从最低点 “倒流” 到其它点(也就是找能流到这个最低点的所有点),将沿路的点全部由标记。这样重复多次,当矩阵全部被标记完成时,我们的任务就完成了。

如果将数据范围加大,可能需要更加优秀的算法,但我目前暂时没有什么头绪。如果对于本题有更优秀的思路,欢迎在评论中发表见解。

代码

#include <queue>
#include <cstdio>
#include <cstring>
#define rr register

struct point {
    int x, y;
    point() {}
    point(int x_, int y_) : x(x_), y(y_) {}
};

const int MAXN = 50 + 5;
int n, m, t, a[MAXN][MAXN];

const int dx[4] = {1, 0,  0, -1};
const int dy[4] = {0, 1, -1,  0};
bool f[MAXN][MAXN];

void BFS(point st)
{
    std::queue <point> q;

    q.push(st);
    t += (f[st.x][st.y] = true);

    int pre = -1;
    while (!q.empty())
    {
        point u = q.front();
        q.pop();

        for (int i = 0; i < 4; ++i)
        {
            point v(u.x + dx[i], u.y + dy[i]);

            if (v.x < 1 || v.x > n || v.y < 1 || v.y > m) continue; //是否合法
            if (a[v.x][v.y] < a[u.x][u.y]) continue; //是否能倒流
            if (i + pre == 3) continue; //是否在回退
            if (f[v.x][v.y]) continue; //是否已访问

            q.push(v);
            t += (f[v.x][v.y] = true);
        }
    }
}

int main()
{
    int arr;
    scanf("%d", &arr);

    for (int cnt = 0; cnt < arr; ++cnt)
    {
        scanf("%d%d", &n, &m);
        memset(a, 0, sizeof(a));

        for (rr int i = 1; i <= n; ++i)
        {
            char c = getchar();
            for (; c < 'a' || c > 'z'; c = getchar());
            for (rr int j = 1; j <= m; ++j)
            {
                a[i][j] = c - 'a';
                c = getchar();
            }
        }

        memset(f, 0, sizeof(f));

        int ans = 0;
        for (t = 0; t < n * m; ++ans)
        {
            int minv = 0x7f7f7f7f;
            point minc(0, 0);
            for (rr int i = 1; i <= n; ++i)
                for (rr int j = 1; j <= m; ++j)
                    if (!f[i][j] && minv > a[i][j]) {
                        minv = a[i][j];
                        minc = point(i, j);
                    }
            BFS(minc);
        }
        printf("%d\n", ans);
    }
    return 0;
}

T4 cowtract (网络)

题目地址

题意概括 。有一个 n 条边、m 条边的无向图,每条边有一个权值。现要从中选取若干条边建图,并且要求图 必须连通 ,而且 不能有环 ,求权值和最大的方案。

**分析** 。注意到上面我加粗的八个黑字,懂一点图论常识都会立马想到,“这不就是树吗?” 确实,本题的题意概括起来,其实就是 “最 "大" 生成树” ,那是不是可以直接用 ```Kruskal``` 或者 ```Prim``` 来做呢?真的可以! 本题就是一个最 “小” 生成树的模板,但有一个值得注意的地方 —— 图不能连通的情况要特殊判断。如果你和我一样莽撞,可能就直接想 “它一开始给的边不满 $n-1$ 条边,就肯定无法连通了吧?” 但实际上还有另外一种情况,见下图。 ![](https://cdn.luogu.com.cn/upload/pic/68209.png) 显然,这个图有 $n-1$ 条边,但它无法连通,所以没法直接用初始的边数来判断。 但是,利用最小生成树算法的性质,我们有更轻松的方式来解决这个疑难问题 —— 只看最后算法选出的边数是否等于 $n-1$ 条边即可。显然,当图像上面一样分成几 “块” 时,没法选出 $n-1$ 条边。 **代码** 。下面使用了 ```Kruskal```。 ```cpp #include <cstdio> #include <cstring> #include <algorithm> //Templete int readint() { int x = 0; char c = getchar(); for (; c < '0' || c > '9'; c = getchar()); for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 3) + (x << 1) + (c ^ '0'); return x; } void outpint(int x) { if (x > 9) outpint(x / 10); putchar(x % 10 + '0'); } //Global const int MAXN = 1000 + 5; const int MAXM = 20000 + 5; int n, m; //Graph int tot; struct edge { int from, to, cost; edge() {} edge(int _from, int _to, int _cost) { from = _from; to = _to; cost = _cost; } } e[MAXM]; bool cmp(edge x, edge y) { return x.cost > y.cost; } //Union Find int root[MAXN]; int getRoot(int x) { return root[x] == -1 ? x : root[x] = getRoot(root[x]); } int main() { n = readint(); m = readint(); for (int i = 1; i <= m; ++i) { int u = readint(); int v = readint(); int w = readint(); e[i] = edge(u, v, w); } std::sort(e + 1, e + m + 1, cmp); memset(root, -1, sizeof(root)); int ans = 0; int tot = 0; for (int i = 1; i <= m; ++i) { int uRoot = getRoot(e[i].from); int vRoot = getRoot(e[i].to); if (uRoot != vRoot) { root[uRoot] = vRoot; ans += e[i].cost; ++tot; } } if (tot != n - 1) puts("-1"); else outpint(ans); return 0; } ``` # 比赛总结 ### 总结题目的推导过程 ### 概括今天收获的重点