PDSU-ACM 2024 第四次周赛 题解

目录/题目列表


注:本题解仅供参考

题解中所有代码均省略头文件,以及添加了宏定义#define int long long

代码中std::可以去掉,只要加上“using namespace std;”就行。

G题与并查集维护相关的知识点为并查集的重点


如果有疑问欢迎评论或找本人询问


A 神仙说他也不懂(签到/模拟)

B 树状数组(模拟)

C 回滚莫队(思维)

D LYS博弈(思维)

E 网络流(并查集-板子)

F 最近公共祖先(思维/并查集)

G 树链剖分 (并查集、完全图、思维)


A 神仙说他也不懂(签到/模拟)


题面:


思路:

          按题目的信息,奇数位的字符串输出第0位,偶数位的字符串输出第1位即可。


AC代码:

void solve()
{
    int n;
    std::string s;
    std::cin >> n;
    for (int i = 1; i <= n; i++)
    {
        std::cin >> s;
        if (i % 2)
            std::cout << s[0];
        else
            std::cout << s[1];
    }
}

B 树状数组(模拟)


题面:


思路:

          原本这题其实是前缀和,但想了想也没啥新颖的改法,于是变成普通的遍历求和做法,只需注意保留小数就可以

          根据动量守恒:m1v1=(m1+m2)v2 就可以求出结果,并且注意题目要求的是第i个小球


AC代码:

void solve()
{
    int n, v, q;
    std::cin >> n >> v >> q;
    std::vector<int> a(n + 1, 0LL);
    for (int i = 1; i <= n; i++)
    {
        std::cin >> a[i];
        a[i] += a[i - 1];
    }
    std::cout << std::fixed << std::setprecision(2) << a[1] * v * 1.0 / (a[q]);
}

C 回滚莫队(思维)


题面:


思路:

先看每个Ai代表什么,代表的是相邻两位的和

由提示给出的表盘可知,转n次正好能转一圈

那么A数组就包含了a1+a2,a2+a3,……,an-1+an,a1+an(a代表表盘上原本的数字),

所以A数组的总和,即A1+A2+......An,就等于2  *(a1+a2+.......+an

那么平均数就很好求了

这道题过程中可能会爆long long,所以#define int long long 是个好习惯(仅打比赛上的)


AC代码:


void solve()
{
    int n;
    std::cin >> n;
    int sum = 0;
    for (int i = 1; i <= n; i++)
    {
        int x;
        std::cin >> x;
        sum += x;
    }
    std::cout << (sum / 2) / n << "\n";
}

D LYS博弈(思维)


题面:


思路:

很典的一道思维题,原题为【牛客】象棋

炮是隔一棋吃一个,根据题意要求列出三种情况:

只有一炮,则剩一炮

单行或单列的情况,有两炮交替进攻吃棋,则剩两炮

多行多列,每行每列各两炮交替进攻吃棋,则剩四炮.


AC代码:

void solve()
{
    int n, m;
    std::cin >> n >> m;
    if (n >= 2 && m >= 2)
        std::cout << "4\n";
    else if (n == 1 && m == 1)
        std::cout << "1\n";
    else
        std::cout << "2\n";
}

E 网络流(并查集-板子)


题面:


思路:

并查集找到联通块的个数,答案就是联通块个数减一

因为这是无向图,所以就不用dfs跑个数了,直接裸的并查集就好了


AC代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,i,x,y,sum,f[1000000];
int find(int x){
    return f[x]==x?x:f[x]=find(f[x]);//找老大
}
int main()
{
cin>>n>>m;
for(i=1;i<=n;i++)f[i]=i;
    sum=n-1;
    while(m--){
      cin>>x>>y;
      if(find(x)!=find(y))//祖宗不同,sum--
          sum--,f[f[x]]=f[y];//设x的老大为y的一个老大
    }
cout<<sum;
}

#include <bits/stdc++.h>
const int N=100010;
int h[N];

int find(int x)
{
    if(h[x]!=x) h[x]=find(h[x]);
    return h[x];
}

int main()
{
    int n,m;
    std::cin>>n>>m;
    for(int i=1;i<=n;i++) h[i]=i;
    while(m--){
        int x,y;
        std::cin>>x>>y;
        int nx=find(x),ny=find(y);
        if(nx!=ny){
            h[nx]=ny;
        }
    }
    int res=0;
    for(int i=2;i<=n;i++){
        int x=find(h[i-1]),y=find(h[i]);
        if(x!=y){
            h[x]=h[y];
            res++;
        }
    }
    std::cout<<res<<"\n";
    return 0;
}

F 最近公共祖先(思维/并查集)


题面:


思路:


AC代码:

#include<bits/stdc++.h>
using namespace std;
int x[1005],y[1005];
int main(){
    int n,x1,y1,ans=0;
    cin >> n;
    while(n--){
        cin>>x1>>y1;
        if(!x[x1]&&!y[y1])  ans++;
        if(x[x1]&&y[y1])    ans--;
        x[x1]++,y[y1]++;
    }
    cout<<ans-1<<endl;
}

G 树链剖分 (并查集、完全图、思维)


题面:


思路&&题意分析:

题目中的“如果顶点u能到达顶点v(通过其他点到达也算),则u到v之间需要加一条边”意思是说:

图中如果有一部分有连通,则就将这部分变为完全图,即这部分中每两个点之间都要有一条边

如图中所示,设黑色边为原来已连接的边紫色为后面需要补上的边。

可以明显看出,图中一共8个点,5条边,被分为3条边,从左到右,我们依次分为1、2、3号连通块。

1、点4是一个单独的点,它不与任何点相连,自然也无法去往任何点,则无需加边


2、1,2,3三个点中,有1-2、1-3两条边。

      因为是无向图,所以1,2,3三个点能相互到达

      所以,它们三个中,每个点之间互相要有一条边。 5、6、7、8三个点同理。

3、图中三个连通块之间互不相连,那也就互相无法到达无需加边

也就是说,我们只需要单独考虑每个连通块内加边的情况,最后统计相加所有连通块的加边数量。

综上所述,

刚开始连在一起的点共同组成一个连通块;

一个连通块内的点才有加边的可能

我们需要将每个连通块单独考虑计算,最后求和。


如果每个连通块有n个点,则将该连通块变为完全图后的边数应为n  *(n-1)/2

只有一个点的连通块不用管。

那么答案就是找出每个连通块,记录下两个值,分别是 :

连通块内原来的边数设为X);

该连通块的点数---->该连通块变为完全图的总边数设为Y);

然后将每个连通块内的Y-X相加就可得出结果。

还要记录哪个连通块是否遍历过,一般都以每个连通块根节点进行结算。


关于并查集的维护与传递:

1、初始化

      最开始,每个点都是一个独立的连通块;

      因此我们将每个点的根节点都设为它本身,并且点数为1,边数为0;

i123
a[i](父节点)123
s[i](边数)000
as[i](点的个数)111

2、两个连通块的链接:

      (1)两个单独的点相连

               很明显,将一个点的父节点改为另一个父节点即可。

               原来这两个点不在一个连通块中,因此需要点数加1。

               题目不会给重边,因此边数直接加1即可。

     

        (2)一个点与一个包含多点连通块相连

                如果要将连通块内所有点的父节点都一个个改动,显然是非常麻烦的,并且时间耗费也很多。

                但我们知道,连通块的所有点都有一个共同的父节点;

                因此,我们可以用它的父节点替代这个连通块,并且这个父节点只需find操作就可以找出,就可以像第(1)种一样进行计算。


                


       请注意理解此处的边和点数是如何继承转换的!!!

        意思是1,2所在的连通块,在与3合并后,父节点变为了3,那么今后要求这个连通块的边数和点的个数都要看s[3]和as[3]的值;

        这个过程中,s[3]=s[2](也就是另一个连通块的父节点)+s[3]+1;

        as[3]=as[2]+as[3]+1;


 

     (3)两个连通块相连

                和第(2)种情况一样,将两个连通块都看作一个单独的结点,最终的操作其实就是对两个连通块父节点进行操作。


AC代码:

注: 学校oj上版本过低,无法使用auto达到在函数里写函数的效果;

请自行修改,将所涉及函数写到主函数外,数组最好也开在外面。


const int N=1e5+10;
int a[N],s[N],as[N];
bool st[N];
/*
    //主函数内写法
    std::vector<int> a(n + 1, 0LL), s(n + 1, 0LL), as(n + 1, 0LL);
    //a数组为并查集数组,s记录每个连通块的边数,as记录每个连通块的点的个数
    std::vector<bool> st(n + 1, false);//方便最后记录哪个连通块没有被遍历
*/
int find(int u)
{
    if(a[u]!=u) a[u]=find(a[u]);
    return a[u];
}
    /*
    //主函数内写法
    auto find = [&](auto find, int u) -> int    //函数内写函数的方法,学校oj不适用
    {
        return a[u] = a[u] == u ? u : find(find, a[u]);
        //“ ? : ”为三目表达式,请自行学习了结。
    };
    */

void merge(int x,int y)
{
    x = find(x);//求出x,y连通块根节点
    y = find(y);
    if (x == y)//如果两个点已经在一个连通块内,退出
        return;
    as[y] += as[x];//因为要吧x所在连通块链接到y上,所以把x的点数转给y
    a[x] = y;//链接两个连通块
}
/*
auto merge = [&](int x, int y) -> void
{
    //主函数内写法
    x = find(x);//求出x,y连通块根节点
    y = find( y);
    if (x == y)//如果两个点已经在一个连通块内,退出
        return;
    as[y] += as[x];//因为要吧x所在连通块链接到y上,所以把x的点数转给y
    a[x] = y;//链接两个连通块
};
*/



void solve()
{
    int n, m;
    std::cin >> n >> m;
    for (int i = 1; i <= n; i++)
        a[i] = i, as[i] = 1LL;
    //初始化,每个点的父节点均为自身,以及它们最初都是单独的一个连通块,都只有一个点。
    while (m--)
    {
        int u, v;
        std::cin >> u >> v;
        u = find(u);//取两个连通块根节点
        v = find(v);
        if (u == v)//如果根节点相同,说明这两个点在一个连通块内,该连通块边数+1即可。
            s[u]++;
        else
        {
            int x = s[u], y = s[v];
            merge(u, v);
            s[find(u)] = x + y + 1;//否则将两个连通块的边数相加再+1
        }
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        int j = a[find(i)];//每个连通块中,所有点的找到的根节点都一样,据此进行遍历判断
        if (st[j] || as[j] <= 1LL)//遍历过或单个点不再计算
            continue;
        ans += ((as[j] * (as[j] - 1)) / 2) - s[j];//思路中的公式:完全图应有的边数-原来的边数。
        st[j] = true;//标记已遍历过
    }
    std::cout << ans;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值