周三的课主要讲了图和树的性质及应用,还介绍了ds中的一个非常重要的点即并查集。
文章目录
知识回顾
图的存储方式
链式前向星
- 一个head[MAXN] 数组,一个边数组Edge[MAXM]
- head[u] 的含义是一条从u 射出的边的编号id,则Edge[id] 即是那条边
- Edge 结构体的内容{ int v, w, nxt} ,nxt是下一条边的编号
链式前向星使用数组来模拟链表,相比较邻接链表而言,边在链式前向星中存储的相对顺序与输入顺序 相反,其他差异不大。使用一个结构体数组Edge[M] 记录每条边的信息以及next来指向下一 条边的(下标)。使用一个数组Head[N] 表示以每个顶点为头的第一条边在结构体数组 中的下标,Head初始化为-1。插入一条边的时候,插入位置为链表的头部:Edge数组的next指向 Head[u]然后Head[u]更新为新加入边的下标
struct Edge
{
int u,v,w,nxt;
}Edges[MAXN];
int head[MAXN],tot;
void init(int n)
{
tot=0;
memset(head,-1,sizeof(head));
}
void addEdge(int u,int v,int w){
Edges[tot].u=u;
Edges[tot].v=v;
Edges[tot].w=w;
Edges[tot].nxt=head[u];
head[u]=tot;
tot++;
}
图的遍历
DFS/BFS
求直径
- 首先明确一点是树的直径一定是某两个叶子之间的距离
- 从树中任选一个点开始遍历这棵树,找到一个距离这个点最远的叶子, 然后再从这个叶子开始遍历,找到离这个叶子最远的另一个叶子,他 俩之间的距离就是树的直径。
- 两次遍历即可求的树的直径。
- 遍历可以用DFS也可以用BFS,找到距离起点最远的叶子结点就好
A题 氪金带东
题目描述:原有电脑编号为1,现购入N-1台,每台电脑用网线接入到一台先前安装的电脑上。求第i台电脑到其他电脑的最大网线长度。
思路:利用求直径的方式。先利用dfs找到距离当前点最远的点作为v1,然后找到距离v1最远的点v2,即直径。求出直径后,最大网线长度就是到v1的距离或者到v2的距离。所以一共需要三次dfs
- 第一次dfs找到距离任意点最远的一个点记为v1
- 第二次dfs找到直径的另一端点v2,并更新点到v1的距离
- 第三次dfs寻找点到v2的距离
在存储图的时候利用了链式前向星
#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
int head[10005],dis1[10005],dis2[10005],dis3[10005];
int n,v1,v2,tot,ans1,ans2;
bool vis[10005];
struct Edge
{
int u,v,w,nxt;
}Edges[20010];
void addEdge(int u,int v,int w){
Edges[tot].u=u;
Edges[tot].v=v;
Edges[tot].w=w;
Edges[tot].nxt=head[u];
head[u]=tot;
tot++;
}
void init(){
tot=0;
ans1=0,ans2=0;
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
memset(dis1,0,sizeof(dis1));
memset(dis2,0,sizeof(dis2));
memset(dis3,0,sizeof(dis3));
}
void dfs(int u)
{
vis[u]=true;
for(int i=head[u];i!=-1;i=Edges[i].nxt){
if(!vis[Edges[i].v])
{
vis[Edges[i].v]=true;
dis1[Edges[i].v]=dis1[u]+Edges[i].w;
if(dis1[Edges[i].v]>ans1)
{
ans1=dis1[Edges[i].v];
v1=Edges[i].v;
}
dfs(Edges[i].v);
}
}
}
void dfs2(int u)
{
vis[u]=true;
for(int i=head[u];i!=-1;i=Edges[i].nxt){
if(!vis[Edges[i].v])
{
vis[Edges[i].v]=true;
dis2[Edges[i].v]=dis2[u]+Edges[i].w;
if(dis2[Edges[i].v]>ans2)
{
ans2=dis2[Edges[i].v];
v2=Edges[i].v;
}
dfs2(Edges[i].v);
}
}
}
void dfs3(int u)
{
vis[u]=true;
for(int i=head[u];i!=-1;i=Edges[i].nxt)
{
if(!vis[Edges[i].v])
{
vis[Edges[i].v]=true;
dis3[Edges[i].v]=dis3[u]+Edges[i].w;
dfs3(Edges[i].v);
}
}
}
int main(){
int v=0,w=0;
while(~scanf("%d",&n))
{
init();
for(int i=2;i<=n;i++)
{
scanf("%d%d",&v,&w);
addEdge(i,v,w);
addEdge(v,i,w);
}
dfs(1);
memset(vis,0,sizeof(vis));
dfs2(v1);
memset(vis,0,sizeof(vis));
dfs3(v2);
for(int i=1;i<=n;i++)
{
int end=max(dis2[i],dis3[i]);
printf("%d\n",end);
}
}
return 0;
}
并查集
并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地进行:查询元素A和元素B是否属于同一组;合并元素A和元素B所在的组。
并查集的利用类似树形的结构实现,只关心其挂载的根节点即代表元。
主要操作:
初始化
void init(int n)
{
for(int i=0;i<=n;i++)
par[i]=i;
}
查找
int find(int x)
{
return par[x]==x? x: par[x]=find(par[x]);
}
T4一开始采用了另一种写法导致无限TLE
int find(int x)
{
if(par[x]==x)
return x;
return find(par[x]);
}
这种写法看起来跟之前的很像但实际缺少了一步优化即将所有的元素都挂载在根节点上,将树压缩为2层,便于之后的查询。
合并
bool unite(int x,int y)
{
x=find(x);
y=find(y);
if(x==y) return false;
par[x]=y;
rank[y]+=rank[x];
return true;
}
B 戴好口罩(模板题)
题目描述:小A编号为0,A达到的群体会被感染,青找到所有和小A直接或间接接触过的同学数量。
思路:利用并查集,将所有小A所在的集合合并,同时将其中元素所在的其他集合也进行合并。对于该题我们加入秩rank来表示每个集合中的元素数量,便于求出答案。
#include <stdio.h>
using namespace std;
int par[30005],rank[30005];
void init(int n)
{
for(int i=0;i<n;i++)
{
rank[i]=1;
par[i]=i;
}
}
int find(int x)
{
return par[x]==x? x: par[x]=find(par[x]);
}
bool unite(int x,int y)
{
x=find(x);
y=find(y);
if(x==y) return false;
par[x]=y;
rank[y]+=rank[x];
return true;
}
int main()
{
int n,m;
while(~scanf("%d %d",&n,&m)&& !(n==0&&m==0))
{
init(n);
for(int i=0;i<m;i++)
{
int num=0,last=-1;
scanf("%d",&num);
for(int j=0;j<num;j++)
{
int tmp=0;
scanf("%d",&tmp);
if(last!=-1) unite(tmp,last);
last=tmp;
}
}
printf("%d\n",rank[find(0)]);
}
return 0;
}
最小生成树Kruskal
实际上也是一种贪心,每次将图中最小的非树边标记为树边,非法则跳过。
- 将全部边按照权值由小到大排序。
- 按顺序(边权由小到大的顺序)考虑每条边,只要这条边和我们已经选 择的边不构成圈,就保留这条边,否则放弃这条边。
- 成功选择(n-1)条边后,形成一棵最小生成树,当然如果算法无法选择出 (n-1)条边,则说明原图不连通
在实现上可以利用并查集进行实现,如果可以加入到树中,就将边所在的集合并入树集中。
C 掌握魔法の东东
题目描述:有n块编号为1~n的农田,灌溉方式有两种,一种是直接浇水,消耗为Wi(i为农田编号),也可以引用其他田的水,消耗为Pij,(ij为农田编号),求为所有田灌溉的最小消耗。
思路:既然直接浇水也会有消耗,不妨将之看做是0号点到各个田的消耗,将问题统一,变成选择边,使得所有边的总和最小,即最小生成树问题。
#include <stdio.h>
#include <algorithm>
using namespace std;
int n,tot=0;
struct Edge
{
int u,v,w;
bool operator <(const Edge e) const
{
return w<e.w;
}
}Edges[305*305];
int par[305];
void init(int n)
{
for(int i=0;i<=n;i++)
par[i]=i;
}
int find(int x)
{
return par[x]==x? x: par[x]=find(par[x]);
}
bool unite(int x,int y)
{
x=find(x);
y=find(y);
if(x==y) return false;
par[x]=y;
return true;
}
int kruskal()
{
init(n);
sort(Edges,Edges+tot);
int p=0,ans=0;
for(int i=0;i<tot;i++)
{
if(unite(Edges[i].u,Edges[i].v))
{
ans+=Edges[i].w;
p++;
if(p==n) return ans;
}
}
return -1;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&Edges[tot].w);
Edges[tot].u=0;
Edges[tot].v=i;
tot+=1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d",&Edges[tot].w);
if(i==j) continue;
Edges[tot].u=i;
Edges[tot].v=j;
tot+=1;
}
}
printf("%d",kruskal());
return 0;
}
/*4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0*/
D 数据中心(CSP 201812-4)
题意很复杂,看懂后发现就是利用给定点求解一棵生成树,使得最大边权最小,求出最大的这条边。
思路一:
利用kruskal最小生成树的性质,最后加入的边长最大,最小生成树一定为瓶颈生成树,直接输出最大边即可。
#include <stdio.h>
#include <algorithm>
using namespace std;
int n,tot=0,m,rt;
struct Edge
{
int u,v,w;
bool operator <(const Edge e) const
{
return w<e.w;
}
}Edges[100005];
int par[100005];
int find(int x){ return par[x]==x? x: par[x]=find(par[x]);}
bool unite(int x,int y)
{
x=find(x);
y=find(y);
par[x]=y;
}
int main()
{
scanf("%d%d%d",&n,&m,&rt);
for(int i=0;i<m;i++)
scanf("%d%d%d",&Edges[i].u,&Edges[i].v,&Edges[i].w);
for(int i=0;i<n;i++)
par[i]=i;
sort(Edges,Edges+m);
int p=0,ans=-10;
for(int i=0;i<m;i++)
{
if(find(Edges[i].u)!=find(Edges[i].v))
{
unite(Edges[i].u,Edges[i].v);
ans=max(ans,Edges[i].w);
p++;
}
if(p==n-1) break;
}
printf("%d",ans);
return 0;
}
思路二:
看到“求解一颗生成树,使得最大边权最小!”最大值最小,很容易想到二分。
二分答案ans,问题转化为原图中不大于ans的边能否使原图联通,二分答案复杂度O(logK),判断连通性复杂度O(N+M),总复杂度O((N+M)logK)
限时大模拟
题目描述:
东东有 A × B 张扑克牌。每张扑克牌有一个大小(整数,记为a,范围区间是 0 到 A - 1)和一个花色(整数,记为b,范围区间是 0 到 B - 1。
扑克牌是互异的,也就是独一无二的,也就是说没有两张牌大小和花色都相同。
“一手牌”的意思是你手里有5张不同的牌,这 5 张牌没有谁在前谁在后的顺序之分,它们可以形成一个牌型。 我们定义了 9 种牌型,如下是 9 种牌型的规则,我们用“低序号优先”来匹配牌型,即这“一手牌”从上到下满足的第一个牌型规则就是它的“牌型编号”(一个整数,属于1到9):
同花顺: 同时满足规则 5 和规则 4.
炸弹 : 5张牌其中有4张牌的大小相等.
三带二 : 5张牌其中有3张牌的大小相等,且另外2张牌的大小也相等.
同花 : 5张牌都是相同花色的.
顺子 : 5张牌的大小形如 x, x + 1, x + 2, x + 3, x + 4
三条: 5张牌其中有3张牌的大小相等.
两对: 5张牌其中有2张牌的大小相等,且另外3张牌中2张牌的大小相等.
一对: 5张牌其中有2张牌的大小相等.
要不起: 这手牌不满足上述的牌型中任意一个.
现在, 东东从A × B 张扑克牌中拿走了 2 张牌!分别是 (a1, b1) 和 (a2, b2). (其中a表示大小,b表示花色)
现在要从剩下的扑克牌中再随机拿出 3 张!组成一手牌!!需要帮他算一算 9 种牌型中,每种牌型的方案数。
input:
第 1 行包含了整数 A 和 B (5 ≤ A ≤ 25, 1 ≤ B ≤ 4).
第 2 行包含了整数 a1, b1, a2, b2 (0 ≤ a1, a2 ≤ A - 1, 0 ≤ b1, b2 ≤ B - 1, (a1, b1) ≠ (a2, b2)).
思路:
由于数据集较小,最多一次100张所以可以采用暴力的方式,三重循环取出三张牌,判断与原来的两张牌共同构成的牌型是那种即可。
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
struct card
{
int number,flow;
bool operator < (const card &p) const
{
if(number!=p.number) return number<p.number;
else return false;
}
}AB[104];
int A,B;
card a,b;
card now[5];
int nownum[30];
int nowflow[5];
int ans[10];
bool shunzi(int nownum[30])
{
for(int h=0;h<=20;h++)
{
if((nownum[h]==1&&nownum[h+1]==1)&&(nownum[h+1]==1&&nownum[h+2]==1)&&
(nownum[h+2]==1&&nownum[h+3]==1)&&
(nownum[h+3]==1&&nownum[h+4]==1))
return true;
}
return false;
}
int main()
{
scanf("%d %d",&A,&B);
scanf("%d %d %d %d",&a.number,&a.flow,&b.number,&b.flow);
int tmp=0;
for (int i=0;i<=A-1;i++)
for(int j=0;j<=B-1;j++)
{
AB[tmp].number=i;
AB[tmp].flow=j;
tmp++;
}
for(int i=0;i<tmp;i++)
{//将取到的两张牌的大小和花色值置成大值,这样在排序的时候会放在最右,tmp-=2即为剩下可抽取牌。
if((AB[i].number==a.number&&AB[i].flow==a.flow)||(AB[i].number==b.number&&AB[i].flow==b.flow))
{
AB[i].number=1000;
AB[i].flow=100;
}
}
sort(AB,AB+tmp);
tmp-=2;
for(int i=2;i<tmp;i++)
for(int j=1;j<i;j++)
for(int k=0;k<j;k++)
{
memset(now,0,sizeof(now));
memset(nownum,0,sizeof(nownum));
memset(nowflow,0,sizeof(nowflow));
now[0]=a;
now[1]=b;
now[2]=AB[k];
now[3]=AB[j];
now[4]=AB[i];
sort(now,now+5);
for(int l=0;l<5;l++)
{
nownum[now[l].number]++;//存储每个数字的牌有几张
nowflow[now[l].flow]++;//存储每种花色的牌有几张
}
int tmp2=0,tmp3=0,tmp4=0;
for(int m=0;m<25;m++)
{
if(nownum[m]==2) tmp2++;
if(nownum[m]==3) tmp3++;
if(nownum[m]==4) tmp4++;
}
bool shun=shunzi(nownum);
int tonghua=0;
for(int n=0;n<5;n++)
{
if(nowflow[n]==5) tonghua=1;
}
if(tonghua==1&&shun) ans[1]++;
else if(tmp4==1) ans[2]++;
else if(tmp2==1&&tmp3==1) ans[3]++;
else if(tonghua==1) ans[4]++;
else if(shun) ans[5]++;
else if(tmp3==1) ans[6]++;
else if(tmp2==2) ans[7]++;
else if(tmp2==1) ans[8]++;
else ans[9]++;
}
for(int i=1;i<=9;i++)
{
if(i!=9)
printf("%d ",ans[i]);
else printf("%d",ans[i]);
}
return 0;
}