目录/题目列表
注:本题解仅供参考题解中所有代码均省略头文件,以及添加了宏定义#define int long long
代码中std::可以去掉,只要加上“using namespace std;”就行。
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;
i 1 2 3 a[i](父节点) 1 2 3 s[i](边数) 0 0 0 as[i](点的个数) 1 1 1 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; }