前言

本次比赛的四道题都是好题,\(T1\)法力水晶是推导题目的性质,如果仔细思考是可以推出的;\(T2\)旗帜是一道斐波那契数列,可以通过高维的\(dp\)状态优化得出;\(T3\)拯救地球是一道 玄学 巧妙的最小生成树,思路虽然比较清奇,但也是可以通过优化的思路推出;\(T4\)回家是一道明显的最短路,建图需要用到分层图的知识点。

然而本人成功地考出了\(0 + 100 + 20 + 60\)的好成绩。可能的原因有:

  1. 一时心急,急于拿到保底的部分分,以至于舍本逐末,没有推出显然的正解;

  2. 没有策略,没有分配好每道题的时间,导致不停地在题目之间切换,无法连续地思考;

  3. 过于看重成绩,重视部分分而非正解,将原本可以写出正解的题目写炸,导致成绩原地升天。

\(T1\) 法力水晶

题目大意

给定一个长度不超过\(1000000\)的数组\(a\),如果\(a\)中相邻的元素和为奇数则会发生“法力碰撞”,参与碰撞的元素会两两消失。试求所有法力碰撞发生后,数组\(a\)中留下的元素个数。

解题思路

显然,如果数组\(a\)中存在一个奇数元素和一个偶数元素,则这两个元素一定会发生法力碰撞。因此,当所有的法力碰撞全部发生后,数组\(a\)中的元素一定奇偶性相同。每一次法力碰撞都会令一个奇数元素和一个偶数元素消失。因此,令\(odd\)为数组\(a\)中奇数元素的个数,\(ever\)为数组\(a\)中偶数元素的个数。法力碰撞的发生次数即为\(min(odd, ever)\),即剩下\(\left| odd - ever \right|\)个元素。

参考代码

在此附上本人的\(AC\)代码,仅供参考,请勿抄袭:

#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
 
int main()
{
    int n;
    int odd = 0, ever = 0, temp;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &temp);
        if (temp % 2)
            odd++;
        else
            ever++;
    }
    printf("%d\n", abs(odd - ever));
    return 0;
}
\(T2\) 旗帜

题目大意

给定三种颜色不同的彩带:白色、蓝色、红色。现在要求相邻的旗帜不能彩带,且蓝色彩带必须放在白色彩带和红色彩带的中间。试求放置\(n\)条彩带的合法方案总数。\(n \leq 45\)

解题思路

题目中出现了“合法方案总数”这个关键词,可以确定解题使用的算法是动态规划。设\(dp_{i, 0}, dp_{i, 1}, dp_{i, 2}\)分别表示第\(i\)面彩带分别放白色、蓝色、红色彩带的方案总数。分类讨论:

  1. 如果放白色彩带,则上一条彩带必须是红色彩带或者蓝色彩带,即\(dp_{i, 0} = dp_{i - 1, 1} + dp_{i - 1, 2}\)。

  2. 如果放蓝色彩带,则上一条彩带必须是红色或者白色彩带。注意到上一条彩带放白色彩带和放红色彩带实际上是等价的,如果状态转移方程设计成\(dp_{i, 1} = dp_{i - 1, 0} + dp_{i - 1, 2}\),则\(dp_{i + 1, 0}\)和\(dp_{i + 1, 2}\)的时候会发生错误,即统计到了白色彩带的两端是同色彩带的情况。因此,\(dp_{i, 1}\)应该从某个等价的状态之一转移过来,即\(dp_{i, 1} = dp_{i - 1, 0}\)或\(dp_{i, 1} = dp_{i - 1, 2}\)。由于蓝色彩带必须放在白色彩带和红色彩带中间,所以最后统计答案的时候不可以统计上蓝色彩带。

  3. 如果放红色彩带,则上一条彩带必须是白色彩带或者红色彩带,即\(dp_{i, 2} = dp_{i - 1, 0} + dp_{i - 1, 1}\)。

因为蓝色彩带不能放在最后,所以最终答案为\(dp_{i, 0} + dp_{i, 2}\)。

实际上,因为放白色彩带和放红色彩带的情况是等价的。注意到\(dp_{i, 0}\)实际上等于\(dp_{i - 2, 0} + dp_{i - 1, 0}\)。等等,这个式子实际上就是斐波那契数列的递推公式!因此,本题实际上就是求解斐波那契数列的第\(n\)项。因为放一条彩带可以放一条红色彩带或白色彩带,放两条彩带可以放一条红色彩带和一条白色彩带,所以该斐波那契数列的边界条件是\(f_{1} = 2, f_{2} = 2\)。

为什么原本需要将\(dp_{n, 0}\)和\(dp_{n, 2}\)加起来,此处却不用将最终答案乘以二?因为如果斐波那契数列的第\(n\)项表示放一条红色彩带或一条白色彩带的话,边界条件应为\(f_{1} = 1\)。我们的边界条件为\(f_{1} = 2\),实际上已经达到了统计出最后一条彩带放白色彩带和红色彩带的总数的效果,所以不用将最终答案乘以二。

参考代码

在此附上本人的\(AC\)代码,仅供参考,请勿抄袭:

#include <cstdio>
using namespace std;
 
int n;
long long dp[50];
 
int main()
{
    scanf("%d", &n);
    dp[1] = 2;
    dp[2] = 2;
    for (int i = 3; i <= n; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    printf("%lld\n", dp[n]);
    return 0;
}
\(T3\) 拯救地球

吐槽

在笔者看来,这是本次模拟赛中 最玄学 出得最好的一道题目。据说本题是原创题目,怪不得这么 毒瘤,赛时无人\(AC\),并且\(solution\)的高分代码最高只有二十分,第一篇高分代码还是我写的暴力,虽然现在已经换成了本人写的正解

题目大意

给定一个无向带权图和\(q\)次询问,每次给定一个源点和“攻击力”。如果某条边的边权小于等于该次的“攻击力”,则该次可以这条边上行走。试求每次从源点出发,最多能走到多少个顶点。注意,每次询问互相独立,源点也包含在可以到达的顶点范围内。

对于\(20\%\)的数据,\(n \leq 10^{5}, m \leq 2 \times 10^{5}, q \leq 10^{3}, w \leq 10^{9}\)

对于另外\(20\%\)的数据,\(n \leq 10^{5}, m \leq 2 \times 10^{5}, q \leq 10^{3}, w \leq 100\)

再另外\(20\%\)的数据,\(n \leq 10^{3}, m \leq 2 \times 10^{5}, q \leq 10^{5}, w \leq 10^{9}\),保证每次询问输入的源点相同

对于剩余\(40\%\)的数据,\(n \leq 10^{5}, m \leq 2 \times 10^{5}, q \leq 10^{5}, w \leq 10^{9}\)

解题思路

\(20pts\)

显然,对于第一档部分分,我们可以直接\(bfs\)暴力求解。对于每次输入的源点\(x\),我们从\(x\)开始\(bfs\)遍历,依据条件松弛即可。时间复杂度\(O(nq)\),期望得分\(20pts\)。

\(40pts\)

对于第三档部分分,我们可以用动态规划的思路解决。设\(dp_{i}\)为从源点\(x\)到达顶点\(i\)所要经过的最大边权,则\(dp_{i} = min(dp_{i}, max(dp_{j}, w)), (i, j) \in E\)。加上第一档部分分就是\(40pts\)的解法。时间复杂度为\(O(nq)\)或\(O(n log n)\)。

\(100pts\)

对于\(20pts\)的做法,重复计算大量状态是超时的主要原因。假如我们有两个不同的询问,攻击力分别为\(w_{1}\)和\(w_{2}\),且\(w_{1} < w_{2}\),那么以\(w_{1}\)的攻击力可以走过的边,以\(w_{2}\)的攻击力也一定可以走过。这样,我们就没有必要每一次都重新\(bfs\)一次。

我们考虑一次就把所有答案求出,也就是在上一次询问的基础上继续进行松弛。想要达到这种情况,攻击力必须是递增的,否则可能会出现\(w_{1} < w_{2}\),也就是无法走过上一条询问走过的边的情况。因此,我们必须先将询问按攻击力从小到大排序,然后离线处理询问。

既然要维护点与点之间的连通性,那么我们就可以用并查集来维护。每次处理询问,我们从上一次处理到的边开始继续处理,假如当前边的边权小于等于当前的攻击力\(w\),说明这条边可以以\(w\)的攻击力走过,将这条边的两端用并查集合并起来。一直枚举图中的边,直到攻击力小于边权为止。此时,源点\(x\)所在的连通分量大小就是此次询问的答案。因为我们要\(O(m)\)处理所有询问,所以我们应该把所有边按边权大小排序,这样才能在保证正确性的前提下不重复计算。

算法时间复杂度\(O(m log m + q log q)\),为排序边和询问的快排时间复杂度,期望得分\(100pts\)。

参考代码

在此附上本人的参考代码,仅供参考,请勿抄袭:

#include <cstdio>
#include <algorithm>
using namespace std;
 
const int maxn = 1e5 + 5;
const int maxm = 4 * 1e5 + 5;
 
struct node
{
    int x, y, w, id;
}edge[maxm], ques[maxn];
 
int n, m, q;
int fa[maxn], size[maxn], ans[maxn];
 
bool cmp(node a, node b)
{
    return a.w < b.w;
}
 
void init()
{
    for (int i = 1; i <= n; i++)
    {
        fa[i] = i;
        size[i] = 1;
    }
}
 
int get(int x)
{
    if (fa[x] == x)
        return x;
    return fa[x] = get(fa[x]);
}
 
void merge(int x, int y)
{
    x = get(x);
    y = get(y);
    if (x != y)
    {
        fa[y] = x;
        size[x] += size[y];
    }
}
 
int main()
{
    int last = 1;
    scanf("%d%d%d", &n, &m, &q);
    init();
    for (int i = 1; i <= m; i++)
        scanf("%d%d%d", &edge[i].x, &edge[i].y, &edge[i].w);
    for (int i = 1; i <= q; i++)
    {
        scanf("%d%d", &ques[i].x, &ques[i].w);
        ques[i].id = i;
    }
    sort(edge + 1, edge + m + 1, cmp);
    sort(ques + 1, ques + q + 1, cmp);
    for (int i = 1; i <= q; i++)
    {
        for (int j = last; j <= m; j++)
        {
            if (edge[j].w <= ques[i].w)
                merge(edge[j].x, edge[j].y);
            else
            {
                last = j;
                break;
            }
        }
        ans[ques[i].id] = size[get(ques[i].x)];
    }
    for (int i = 1; i <= q; i++)
        printf("%d\n", ans[i]);
    return 0;
}
\(T4\) 回家

题目链接

题目描述

moreD城的城市轨道交通建设终于全部竣工,由于前期规划周密,建成后的轨道交通网络由 2n 条地铁线路构成,组成了一个 n 纵 n 横的交通网。如下图所示,这 2n 条线路每条线路都包含 n 个车站,而每个车站都在一组纵横线路的交汇处。
出于建设成本的考虑,并非每个车站都能够进行站内换乘,能够进行站内换乘的地铁站共有 m 个,在下图中,标上方块标记的车站为换乘车站。已知地铁运行 1 站需要 2 分钟,而站内换乘需要步行 1 分钟。 你的最后一个作业就是算出,在不中途出站的前提下,从学校回家最快需要多少时间(等车时间忽略不计)。

数据范围

对于\(10\%\)的数据,\(m = 0\)

对于\(30\%\)的数据,\(n \leq 50, m \leq 1000\)

对于\(60\%\)的数据,\(n \leq 500, m \leq 2000\)

对于\(100\%\)的数据,\(n \leq 20000, m \leq 100000\)

解题思路

这道题中“最快”“线路”等关键字提示我们,这道题需要用到最短路的知识点。那么,如何建图就是解决本题的关键。

题目中提到,线路分为横和纵两种,并且横线路和纵线路可以在中转站切换,满足分层图将同一个图分成多个层次,并且层次之间可以在特定情况下转移的性质。因此,可以确定本题使用分层图最短路的算法解题。

我们将图分成横和纵两个层次。相应地,点也就分成了行点列点两个层次的点,行点表示行走方向为横向的点,列点表示行走方向为纵向的点。对于中转站,它们可以耗费\(1\)个时间单位转换方向,也就是行点和列点之间有一条边权为\(1\)的连边。对于同一行的中转站,它们的距离为列的差;对于同一列的中转站,它们的距离为行的差。因此,我们可以把点分别按行和列排序,再分别对同一行和同一列的点建边。

起点和终点可以看做是两个特殊的点。因为在起点和终点转换方向不需要时间,所以起、终点拆出的行列点之间的连边边权应为\(0\),否则求出的答案会比正确答案多\(2\)。

参考代码

在此附上本人的\(AC\)代码,仅供参考,请勿抄袭:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <algorithm>
using namespace std;
 
const int maxn = 1000005;
const int inf = 0x3f3f3f3f;
 
struct Edge
{
    int to, nxt, w;
}edge[maxn];
 
struct node
{
    int x, y, id;
}coor[maxn];
 
int n, m, s, t, cnt;
int head[maxn], dis[maxn];
bool vis[maxn];
 
void add_edge(int u, int v, int w)
{
    cnt++;
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].nxt = head[u];
    head[u] = cnt;
}
 
void dijkstra()
{
    priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > pq;
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0;
    pq.push(make_pair(0, s));
    while (!pq.empty())
    {
        int v = pq.top().second;
        pq.pop();
        if (vis[v])
            continue;
        vis[v] = true;
        for (int i = head[v]; i; i = edge[i].nxt)
        {
            if (dis[edge[i].to] > dis[v] + edge[i].w)
            {
                dis[edge[i].to] = dis[v] + edge[i].w;
                pq.push(make_pair(dis[edge[i].to], edge[i].to));
            }
        }
    }
}
 
bool cmpx(node a, node b)
{
    if (a.x != b.x)
        return a.x < b.x;
    return a.y < b.y;
}
 
bool cmpy(node a, node b)
{
    if (a.y != b.y)
        return a.y < b.y;
    return a.x < b.x;
}
 
int main()
{
    scanf("%d%d", &n, &m);
    s = m + 1;
    t = m + 2;
    for (int i = 1; i <= m + 2; i++)
    {
        scanf("%d%d", &coor[i].x, &coor[i].y);
        coor[i].id = i;
    }
    sort(coor + 1, coor + m + 3, cmpx);
    for (int i = 1; i < m + 2; i++)
    {
        if (coor[i].x == coor[i + 1].x)
        {
            int dis = coor[i + 1].y - coor[i].y;
            add_edge(coor[i].id, coor[i + 1].id, dis * 2);
            add_edge(coor[i + 1].id, coor[i].id, dis * 2);
        }
    }
    sort(coor + 1, coor + m + 3, cmpy);
    for (int i = 1; i < m + 2; i++)
    {
        if (coor[i].y == coor[i + 1].y)
        {
            int dis = coor[i + 1].x - coor[i].x;
            add_edge(coor[i].id + m + 2, coor[i + 1].id + m + 2, dis * 2);
            add_edge(coor[i + 1].id + m + 2, coor[i].id + m + 2, dis * 2);
        }
    }
    for (int i = 1; i <= m; i++)
    {
        add_edge(i, i + m + 2, 1);
        add_edge(i + m + 2, i, 1);
    }
    add_edge(m + 1, 2 * m + 3, 0);
    add_edge(2 * m + 3, m + 1, 0);
    add_edge(m + 2, 2 * m + 4, 0);
    add_edge(2 * m + 4, m + 2, 0);
    dijkstra();
    if (dis[t] != inf)
        printf("%d\n", dis[t]);
    else
        printf("%d\n", -1);
    return 0;
}