第六周 并查集、kruskal算法、dfs算法

6.1网线的长度
从1号点开始,进行 bfs 遍历,找到距离最大的点,即为直径的一个端点v1.
图论中, 图的直径是指任意两个顶点间距离的最大值.
(距离是两个点之间的所有路的长度的最小值)
从v1开始进行bfs遍历,找到距离最大的点,即为直径的一个端点v2;

将所有点距离 v1 的长度进行数组存储,将所有点距离 v2 的长度进行数组存储
取两个数组中对应的两个距离中的大值即可。

前向星是一种特殊的边集数组,我们把边集数组中的每一条边按照起点从小到大排序
如果起点相同就按照终点从小到大排序,并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,那么前向星就构造好了.
用sum[i]来记录所有以i为起点的边在数组中的存储长度.
用head[i]记录以i为边集在数组中的第一个存储位置.

其中edge[i].to表示第i条边的终点
edge[i].from表示第i条边的起点
edge[i].w为边权值
edge[i].next表示与第i条边同起点的下一条边的存储位置

另外还有一个数组head[],它是用来表示以i为起点的第一条边存储的位置
实际上你会发现这里的第一条边存储的位置其实,
在以i为起点的所有边的最后输入的那个编号.
head[]数组一般初始化为-1

实现过程
从某一个点开始(这里是0)遍历,寻找最大直径的一个端点v1
从所找到的最大端点v1开始,遍历寻找最大直径的另一个端点v2
用数字sum2、sum3统计图中的点到两个端点的距离
最后输出数组中两个值的最大值即可
寻找最大点的实现:通过加边实现距离的不断更新,进而实现最大端点的更新

#include<iostream> 
using namespace std;
int v1,v2,count,len1,len2,len3;
int head[10010];
int sum1[10010],sum2[10010],sum3[10010];
bool flag1[10010],flag2[10010],flag3[10010];

struct graph//图结构
{
	int from;
	int to;
	int w;
	int next;
}e[20020];

void addedge(int from,int to,int w)//插入边
{
	e[count].from=from;
	e[count].to=to;
	e[count].w=w;
	e[count].next=head[from];
	head[from]=count;
	count++;
}

//遍历第一遍找谁是最大直径的一个端点v1 
void dfs1(int u)
{
	flag1[u]=1;
	for(int i=head[u];i!=-1;i=e[i].next)
	{
		if(!flag1[e[i].to])
		{
			flag1[e[i].to]=1;
			sum1[e[i].to]=sum1[u]+e[i].w;
			if(sum1[e[i].to]>len1)
			v1=e[i].to;
			len1=max(len1,sum1[e[i].to]);
			dfs1(e[i].to);
		}
	}
}

//遍地第二遍找谁是最大直径的第二个端点,同时确定第一个最大端点到所有点的距离
void dfs2(int u)             
{
	flag2[u]=1;
	for(int i=head[u];i!=-1;i=e[i].next)
	{
		if(!flag2[e[i].to])
		{
			flag2[e[i].to]=1;
			sum2[e[i].to]=sum2[u]+e[i].w;
			if(sum2[e[i].to]>len2)
			v2=e[i].to;
			len2=max(len2,sum2[e[i].to]);
			dfs2(e[i].to);
		}
	}
}

//遍历第三遍找第二个端点到其他点的距离
void dfs3(int u)
{
	flag3[u]=1;
	for(int i=head[u];i!=-1;i=e[i].next)
	{
		if(!flag3[e[i].to])
		{
			flag3[e[i].to]=1;
			sum3[e[i].to]=sum3[u]+e[i].w;
			dfs3(e[i].to);
		}
	}
}

int main()
{
	int n;
	while(cin>>n)
	{
		for(int i=0;i<10010;i++)
		{
			sum1[i]=0;
			sum2[i]=0;
			sum3[i]=0;
			flag1[i]=0;
			flag2[i]=0;
			flag3[i]=0;
			head[i]=-1;
		}
		len1=0;
		len2=0;
		len3=0;
		count=0;
		
		for(int i=2;i<=n;i++)
		{
			int v,w;
			cin>>v>>w;
			addedge(i,v,w);//无向图要插入双向
			addedge(v,i,w);
		}
		
		dfs1(1);
		dfs2(v1);
		dfs3(v2);
		for(int i=1;i<=n;i++)
		{
			cout<<max(sum2[i],sum3[i])<<endl;
		}
	}
} 

6.2.病毒传播问题
该问题是统计一个点的间接与直接连接点的问题。
思路:将点连接,就是在点之间不断加边,最后输出对应点所在连通边上点的数目。

实现:发现两个点接触时,判断两点是否连通,若两个点不连通,对两点加边。
加边后,改变原一点的根节点,更新根节点所在边的点数目。
最终输出0号节点对应根节点所在边上点的数目即可。

#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=3e4+10;
int par[maxn]; 
int count[maxn];

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;
	count[y]=count[y]+count[x]; //统计以y为根节点的数目 
	return true;
}

int main()
{
	int n,m;
	while(cin>>n>>m)//n是学生的数量,m是学生群体的数
	{
		if(n==0&&m==0)
		{
			return 0;
		}	
		for(int i=0;i<n;i++)
		{
			par[i]=i;
			count[i]=1;
		}
		for(int i=0;i<m;i++)
		{
			int num;//团体人员数量
			int preson=n;
			scanf("%d",&num);
			for(int i=0;i<num;i++)
			{
				int id;//这个小团体的学生编号 
				scanf("%d",&id);
				if(preson!=n)       //只有当有两个人的时候,才可以进行合并 
				{
					unite(id,preson); 
				}
				preson=id;	
			}
			
		}
		printf("%d\n",count[find(0)]);//0连通的点的数目,放在0的根节点的统计数中 
	}
	return 0;
}
 

6.3.魔法灌溉问题
构建0号区域,将1-n号田的魔法引水转化为0号区域引水
构建出0号区域后,我们就会得到一个n*(n+1)的矩阵
该矩阵的建立目的是实现了图的存储
该问题转化为了,在图中寻找最小连通路径

实现过程:采用kruskal算法
不断的选取图中的最小边,当边所连的两点未连通时,选择该边加入
并将该边的花费进行统计,
当所加边为(n-1)条时,证明n个点已经连通,此时选边终止
统计量ans即为灌溉所有田地的最小花费

#include<iostream>
#include<stdio.h>
#include<algorithm> 
using namespace std;

const int maxn=3e2+10;
int k,n; 

struct edge  //水渠结构体数组
{
    int u,v,w;
    bool operator<(const edge &p)const//按照权值从小到大排序 
	{
		return w<p.w;
	}
}e[maxn*maxn]; 

int node[maxn];   //并查集的节点数组,值代表当前节点的父节点

//并查集的查询算法,目的是看两个点是否连通 
int find( int a) 
{
    return node[a] == a ? a : find(node[a]);
}

//对两个节点合并 
bool unite(int x,int y)
{
	x=find(x);
	y=find(y);
	if(x==y) return false;
	node[x]=y;
	return true;
}

int kruskal()//最小生成树 
{
	 //初始化每个麦田所在树结构的根节点为自身,这里有 0号麦田  
    for(int i=0;i<=n;i++)
    {
        node[i] = i;  
    } 
    
	sort(e,e+k);//排序
	int ans=0;
	int count=0;
	for(int i=0;i<k;i++)
	{	//如果父节点不相同,说明该两个麦田不在一个树中,
		//则把该两个节点合并到一个树中,即使父节点相同,
        if(unite(e[i].u,e[i].v)) 
        {      
		    //建造这两个麦田之间的水渠,因为已经过排序,所以是当前花费最少的一个水渠                                                                    
            //如果两个麦田的根节点相同,说明这两个麦田以通水田,
			//不需要再建立,则跳过该条水渠的建立
			ans+=e[i].w; //增加花费                                    
            count++; //增加水渠数量
        }
 
        if(count==n)
		{
			return ans; 
		} 	
	} 
	return ans;
	
}
 

int main()
{
	//n代表麦田数量   
    scanf("%d",&n);
    //输入每一条可建造的水渠的数据,此时输入时要建立0好原点
	k=0;	 
	for(int i=0;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			scanf("%d",&e[k].w);
			if(i==j) continue;//pii=0 
			e[k].u=i;
			e[k].v=j;
			k++;
		}
	}
 
    int num=kruskal();
    printf("%d",num);
    return 0;
}
 

6.4.数据中心问题
实际上是从最小生成树中,选取最大边
同6.3,不过此时ans不统计所有边的权重和,而是更新统计选取边中权重最大边的权重

#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

const int N=50000+5;
const int M=100000+5;
struct edge
{
 	int u,v,w;
 	bool operator<(const edge &t)
	{ return w<t.w;}
}e[M];
 
int n,m,root;
int par[N];
 
void init(int n)
{
 	for(int i=1;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 main()
{	
	scanf("%d%d%d",&n,&m,&root);
	init(n);
	for(int i=0;i<m;i++)
	{
		scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
	}
 
    sort(e,e+m);
	int cnt=0,ans=0;
	for(int i=0;i<m;i++)
	{
		if(unite(e[i].u,e[i].v))
		{
			ans=max(ans,e[i].w);
			cnt++; 
			if(cnt==n-1)
			{
 	          printf("%d\n",ans);  
 			  return 0; 	
			}
		}
	}
	
	printf("%d\n",-1);
	return 0;
}
 

6.5.关于一手牌的判断与提交
选取5张牌:该问题的解法是通过深度优先搜索的剪枝策略实现

对于所定义的n张牌,处理每一张牌的时候
我们都有两种选择:选/不选该张牌,相当于对于一种选择直接选出一条路径
剪枝化策略:当选牌范围超出最大范围或选够了5张牌,这种路径终止
当遇见选牌为首先拿出的两张牌的时候,对该牌进行跳过

关于牌型的判断实现:
一张牌的特征点为:花色,大小
我们分别对牌面的大小与花色统计,我们初始化牌型为散牌,再进行判断更改

当牌面大小相同牌数最大为3时,判断上一个统计数
当统计数为2时,牌型为3带2(5);否则牌型为3条(7)

当牌面大小相同牌数最大为2时,判断上一个统计数
当统计数为2时,牌型为双对(6);否则牌型为单对(8)

当牌面大小相同牌数最大为4时,此时该牌型为炸弹(4)

判断顺子是通过循环判断5张牌的差值,来进行判断,是顺子则判断为(2)

对花色进行统计,判断同一花色的最大牌数,若为5,则为同花。
在顺子的基础上判断是否为同花,非顺子同花为(3)
顺子同花为(1)

#include<iostream>
#include<algorithm>
using namespace std;

int A,B;    //A 代表牌面大小,B代表牌面花色 

struct card 
{
	int a;
	int b;
	bool operator<(const card &p) const
	{
	  if(a!=p.a) return a<p.a;
	  else       return b<p.b;
	}
}; 

card c[100];           //用于标记所有的牌 
int flag[100]={0};
card five[5];          //用于表示选中的5张牌   

void dfs(int n,int *count,int pai,card *five)
{
    if(n==5)
	{
    	int size[25]={0};
    	int color[4]={0};
    	for(int i=0;i<5;i++)
		{
    		size[five[i].a]++;
    		color[five[i].b]++;
		}
		sort(size,size+25);
		sort(color,color+4);
		
		//判断并确定牌型,初始化为散牌:k=9 
		int k=9;
		if(size[24]==3)
		{
			if(size[23]==2)
			{
				k=5;             //判断是否为三带二 
			}
			else k=7;            //判断是否为三条 
		}
		
		else if(size[24]==2)
		{
			if(size[23]==2)
			{
				k=6;            //判断是否为双对  
			}
			else k=8;           //判断是否为一对 
		}
		
		else if(size[24]==4)
		{
			k=4;                //判断是否为炸弹 
		}
	
		else                    //判断是否为顺子 
		{
			card pfive[5];
			for(int i=0;i<5;i++)
			{
				pfive[i].a=five[i].a;
				pfive[i].b=five[i].b;
			}
			sort(pfive,pfive+5);
			int shun=1;
			for(int i=1;i<5;i++)
			{
				if(pfive[i].a-pfive[i-1].a!=1)
				{
					shun=0;
				}
			}

			if(shun==1)
			{
				k=2;
			}	
		}
		
		if(color[3]==5)          //在顺子的基础上判断是否为同花 
		{
			if(k==2)	k=1;
			else        k=3;
		}
		
		count[k]++;
		return;
	}
	
	if(pai>=A*B)       //当牌已经遍历到循环的最后时,返回 
	{
		return;
	}
	
	if(flag[pai]==1)   //当这张牌是初始的两张牌之一时,跳过取下一张 
	{
		dfs(n,count,pai+1,five);
	}
	else       
	{
	  dfs(n,count,pai+1,five);  //跳过当前牌取下一张进行组合 
	  five[n]=c[pai];           //取当前牌进行组合 
	  dfs(n+1,count,pai+1,five);	
    }
}

int main()
{ 

    cin>>A>>B;
    for(int i=0;i<B;i++)
	{
		for(int j=0;j<A;j++)
		{
			c[i*A+j].a=j;
			c[i*A+j].b=i;
		}
	}
	
	int a1,b1,a2,b2;
	cin>>a1>>b1>>a2>>b2;
	five[0]=c[b1*A+a1];   //对取出的两张牌进行存储 
	five[1]=c[b2*A+a2];
	
	flag[b1*A+a1]=1;      //对取出的两张牌进行标记 
	flag[b2*A+a2]=1;
	
	int n=2;              //用n标记当前取出牌的数目 
	int count[10]={0};    //用count进行牌型计数 
	dfs(n,count,0,five);
	for(int i=1;i<=9;i++)
	{
		cout<<count[i]<<" ";
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值