程序设计思维与实践 week6

作业

A - 氪金带东

题目:

https://vjudge.net/contest/363991#problem/A


题意:

实验室里原先有一台电脑(编号为1),最近氪金带师咕咕东又为实验室购置了N-1台电脑,编号为2到N。每台电脑都用网线连接到一台先前安装的电脑上。但是咕咕东担心网速太慢,他希望知道第i台电脑到其他电脑的最大网线长度,但是可怜的咕咕东在不久前刚刚遭受了宇宙射线的降智打击,请你帮帮他。

输入:包含多组测试数据。对于每组测试数据,第一行一个整数N (N<=10000),接下来有N-1行,每一行两个数,对于第i行的两个数,它们表示与i号电脑连接的电脑编号以及它们之间网线的长度。网线的总长度不会超过10^9,每个数之间用一个空格隔开。
输出:对于每组测试数据输出N行,第i行表示i号电脑的答案 (1<=i<=N).
在这里插入图片描述


思路:

图的遍历/树的直径

电脑之间的连接相当于是一棵树。

首先,将边的信息通过链式前向星存储起来,形成“边池”。

然后通过dfs求得树的直径和直径的两个端点。树的直径就是树中任意两点之间距离的最大值。

树的直径的求法: 首先明确一点是树的直径一定是某两个叶子之间的距离 。从树中任选一个点A开始遍历这棵树,找到一个距离这个点最远的叶子B, 然后再从这个叶子开始遍历,找到离这个叶子最远的另一个叶子C,B,C之间的距离就是树的直径。两次遍历即可求的树的直径。

两点之间距离的存储:需要一个结构体数组D存储到所有顶点的信息。

struct D
{
	int v;	//入点
	bool flag;	//是否被访问过
	int dis;	//到该点的距离 
};
D d[20000];

在dfs的时候,每到一个未访问过的点A(edge[i].v),将其标记为已访问过,而对于点A的邻接点,点u(dfs的起始点)到A的邻接点的距离等于到点A的距离d[edge[i].u].dis加上点A到邻接点的距离edge[i].dis

void dfs(int u)
{
	d[u].flag=1;
	for(int i=head[u];i!=-1;i=edge[i].nxt)
	{
		if(d[edge[i].v].flag==0)	//未被访问过
		{
			d[edge[i].v].flag=1;
			d[edge[i].v].dis=d[edge[i].u].dis+edge[i].dis;
			dfs(edge[i].v);
		} 
	}
}

最后,将数组d[]排序,即可得到距离点u最远的顶点。同理,再进行一遍dfs,就可以得到直径的两端点。

每个点到任意一点的最大距离可归纳为到直径两端点的最大距离。

所以分别对直径的两端点lr进行dfs,求得两端点到其余各个点的距离,并用数组存储。最后再比较得出每个点到直径两端点的最大值即可。


总结:

细节很重要,比如电脑从1开始编号,输入有多组数据需要清空通用数组等等。


代码:
#include <iostream>
#include <cstdlib>
#include <algorithm>
using namespace std;

int n,p,q,l,r;
struct EDGE
{
	int u,v,dis;
	int nxt;
}; 
EDGE edge[20000];
struct D
{
	int v;
	bool flag;
	int dis;
	
	bool operator <(D &d)
	{
		return dis<d.dis;
	}
};
D d[20000];
int head[20000]={-1};
int tot;	//当前插入的边的数量

void ini_d()
{
	for(int i=1;i<=n;i++)
	{
		d[i].v=i;
		d[i].dis=0;
		d[i].flag=0;
	}
}
void initial()
{
	tot=0;
	for(int i=1;i<=n;i++)
		head[i]=-1;
} 
void add_edge(int u,int v,int w)
{
	tot++;
	edge[tot].u=u;
	edge[tot].v=v;
	edge[tot].dis=w;
	edge[tot].nxt=head[u];
	head[u]=tot;
	
}
void dfs(int u)
{
	d[u].flag=1;
	for(int i=head[u];i!=-1;i=edge[i].nxt)
	{
		if(d[edge[i].v].flag==0)	//未被访问过
		{
			d[edge[i].v].flag=1;
			d[edge[i].v].dis=d[edge[i].u].dis+edge[i].dis;
			dfs(edge[i].v);
		} 
	}
}
void diam()
{
	ini_d();	//clear
	dfs(1);
	sort(d+1,d+1+n);
	l=d[n].v;
	
	ini_d();
	dfs(l);
	sort(d+1,d+1+n);
	r=d[n].v;
}
int dd[20000];
void dis_max()
{
	ini_d();
	dfs(l);
	for(int i=1;i<=n;i++)
		dd[i]=d[i].dis;
	ini_d();
	dfs(r);
	for(int i=1;i<=n;i++)
		printf("%d\n",dd[i]>d[i].dis?dd[i]:d[i].dis);
}
int main()
{
	while(~scanf("%d",&n))
	{
		initial();
		
		for(int i=2;i<=n;i++)
		{
			//与i号电脑连接的编号 和之间的距离 
			scanf("%d %d",&p,&q);
			add_edge(i,p,q);	//无向图 
			add_edge(p,i,q);
		}
		//求直径及两端点
		diam();
		//每个点的最长距离 
		dis_max();	
	}
	return 0;
}

B - 戴好口罩!

题目:

https://vjudge.net/contest/363991#problem/B


题意:

新型冠状病毒肺炎(Corona Virus Disease 2019,COVID-19),简称“新冠肺炎”,是指2019新型冠状病毒感染导致的肺炎。
如果一个感染者走入一个群体,那么这个群体需要被隔离!
小A同学被确诊为新冠感染,并且没有戴口罩!

需要尽快找到所有和小A同学直接或者间接接触过的同学,将他们隔离,防止更大范围的扩散。
众所周知,学生的交际可能是分小团体的,一位学生可能同时参与多个小团体内。
请你编写程序解决!戴口罩!!

输入:多组数据,对于每组测试数据:第一行为两个整数n和m(n = m = 0表示输入结束,不需要处理),n是学生的数量,m是学生群体的数量。0 < n <= 3e4 , 0 <= m <= 5e2,学生编号为0~n-1,小A编号为0。随后m行,每行有一个整数num即小团体人员数量。随后有num个整数代表这个小团体的学生。
输出:要隔离的人数,每组数据的答案输出占一行


思路:

并查集

par[]数组用来实现并查集,存储每个元素的祖先。如下图所示:

并查集包括初始化initial(int n),查找所属集合find(int x),合并两个元素所在的集合unite(int x,int y)

首先是初始化并查集,即每个学生都是一个孤立的集合。

然后是合并m个小团体,将每个小团体的第2至最后一个学生所在的集合与第一个学生所在的集合合并。

最后for循环遍历所有学生所在的集合的祖先,如果与0所在的集合的祖先相同,则要隔离人数+1,即count++


总结:

没有一次ac,发现是没仔细看输入格式【捶胸顿足!!!


代码:
#include <iostream>
using namespace std;

int par[100000];
int n,m,num,count=0;

void initial(int n)
{
	for(int i=0;i<n;i++)
		par[i]=i;
}
int find(int x)
{
	if(par[x]==x)
		return x;
	par[x]=find(par[x]);
	return par[x]; 
}
bool unite(int x,int y)
{
	int a=find(x),b=find(y);
	if(a==b)
		return false;
	par[a]=b;
	return true;
}
int main()
{
	while(cin>>n>>m&&(n!=0||m!=0))
	{
		//clear		
		for(int i=0;i<n;i++)
			par[i]=0;
		count=0;
		
		initial(n);
		for(int i=0;i<m;i++)
		{
			cin>>num;
			int a,b;
			cin>>a;
			for(int j=1;j<num;j++)
			{
				cin>>b;
				unite(a,b);
			}
		}
		int tar=find(0);
		for(int i=1;i<n;i++)
		{
			if(tar==find(i))
				count++;
		}
		count++;	//小A自己 
		cout<<count<<endl;
	}
	return 0;
}

C -

题目:

https://vjudge.net/contest/363991#problem/C


题意:

东东在老家农村无聊,想种田。农田有 n 块,编号从 1~n。种田要灌氵
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。 (1<=n<=3e2)
黄河之水天上来的消耗是 Wi,i 是农田编号 (1<=Wi<=1e5)
建立传送门的消耗是 Pij,i、j 是农田编号 (1<= Pij <=1e5, Pij = Pji, Pii =0)
东东为所有的田灌氵的最小消耗

输入:第1行:一个数n;第2行到第n+1行:数wi;第n+2行到第2n+1行:矩阵即pij矩阵
输出:东东最小消耗的MP值


思路:

最小生成树/图的重构

如果没有黄河之水,也不考虑田里有没有水的情况,就是一个求最小生成树的问题。

因为水是必须的,所以如果把黄河水也作为一个点,引水需要的MP就是这个点到每块农田的边的权值,再加上n块农田,就相当于求n+1个点的最小生成树。这就是图的重构

输入的时候把黄河水这个点的边和农田之间的边存储到边集中,再kruskal就好了。

注意:最小生成树和dfs搜索不一样,最小生成树的对象是边,dfs的对象是点。所以在无向图的时候,最小生成树不用把一条边存储两次,但dfs需要。


总结:

上课的时候没怎么听懂,所以一开始自己按自己的想法肆意探索。然后就是,做不出来。

再去复盘上课视频,不禁感慨。天哪加个超级源点重构图到底是谁想出来的!太机智了吧!!!


代码:
#include <iostream>
#include <algorithm>
using namespace std;

int n,tot,a;
struct EDGE
{
	int u,v,w;
	
	bool operator <(EDGE &e)
	{
		return w<e.w;
	}
};
EDGE e[100000];
//并查集 
int par[400];
void initial(int n)
{
	for(int i=0;i<=n;i++)
		par[i]=i;
}
int find(int x)
{
	if(par[x]==x)
		return x;
	return par[x]=find(par[x]);
}
bool unite(int x,int y)
{
	int a=find(x),b=find(y);
	if(a==b)
		return false;
	par[a]=b;
		return true;
}
void kruskal(int n)
{
	initial(n);
	int num=0,sum=0; 	//e[index] 
	for(int i=1;i<=tot;i++)
	{
		if(unite(e[i].u,e[i].v))	//不在同一个集合中
		{
			num++;
			sum+=e[i].w;
		} 
		if(num==n)
		{
			cout<<sum<<endl;
			return;
		}
	}
}
void add_edge(int u,int v,int w)
{
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].w=w;
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a;
		add_edge(0,i,a);
		//add_edge(i,0,a);
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>a;
			if(i!=j)
			{
				add_edge(i,j,a);
			}	
		}
	}
	sort(e+1,e+1+tot);

	kruskal(n);
	return 0;
}


D -

题目:

https://vjudge.net/contest/363991#problem/D


题意:

在这里插入图片描述


思路:

最小生成树

就是求一棵最小生成树。

因为最小生成树是每次贪心尝试将图中最小的非树边标记为树边,非法则跳过。

所以要使生成树中的最大边的权值最小,就是最小生成树。

最大边的权值甚至不需要通过比较给出,最后一条被假如最小生成树的边的权值就是要求的那个。

直接怼kruskal就好啦。


总结:

说实话题目真的没怎么看懂…

一开始并查集写错了还死活de不出bug (〃´皿`)q
所以,已经ac的题目也不要小觑,有空就复习复习上课讲的结构啊算法啊,practice makes perfect!


代码:
#include <iostream>
#include <algorithm>
using namespace std;

int n,m,root,tot;
struct Edge
{
	int u,v,w;
	
	bool operator <(Edge &e)
	{
		return w<e.w;
	}
};
Edge e[200000];
int par[100000];
void initial(int n)
{
	for(int i=1;i<=n;i++)
		par[i]=i;
}
int find(int x)
{
	if(par[x]==x)
		return x;
	par[x]=find(par[x]);
	return par[x]; 
}
bool unite(int x,int y)
{
	int a=find(x),b=find(y);
	if(a==b)
		return false;
	par[a]=b;
	return true;
}
void add_edge(int u,int v,int w)
{
	tot++;
	e[tot].u=u;
	e[tot].v=v;
	e[tot].w=w;
}
void kruskal()
{
	sort(e+1,e+1+m);
	initial(n);
	int ans=0,num=0;
	for(int i=1;i<=m;i++)
	{
		if(unite(e[i].u,e[i].v))
		{
			num++;
			ans=e[i].w;
		}
		if(num==n-1)
		{
			break;
		}
	}
	cout<<ans<<"\n";
}
int main()
{
	cin>>n>>m>>root;
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		add_edge(u,v,w);
	}
	kruskal();
	return 0;
}

CSP - 限时模拟题

A - 掌握魔法の东东 II

题目:

https://vjudge.net/contest/364699#problem/A


题意:

从瑞神家打牌回来后,东东痛定思痛,决定苦练牌技,终成赌神!
东东有 A × B 张扑克牌。每张扑克牌有一个大小(整数,记为a,范围区间是 0 到 A - 1)和一个花色(整数,记为b,范围区间是 0 到 B - 1。
扑克牌是互异的,也就是独一无二的,也就是说没有两张牌大小和花色都相同。
“一手牌”的意思是你手里有5张不同的牌,这 5 张牌没有谁在前谁在后的顺序之分,它们可以形成一个牌型。 我们定义了 9 种牌型,如下是 9 种牌型的规则,我们用“低序号优先”来匹配牌型,即这“一手牌”从上到下满足的第一个牌型规则就是它的“牌型编号”(一个整数,属于1到9):

同花顺: 同时满足规则 2 和规则 3.
顺子 : 5张牌的大小形如 x, x + 1, x + 2, x + 3, x + 4
同花 : 5张牌都是相同花色的.
炸弹 : 5张牌其中有4张牌的大小相等.
三带二 : 5张牌其中有3张牌的大小相等,且另外2张牌的大小也相等.
两对: 5张牌其中有2张牌的大小相等,且另外3张牌中2张牌的大小相等.
三条: 5张牌其中有3张牌的大小相等.
一对: 5张牌其中有2张牌的大小相等.
要不起: 这手牌不满足上述的牌型中任意一个.
现在, 东东从A × B 张扑克牌中拿走了 2 张牌!分别是 (a1, b1) 和 (a2, b2). (其中a表示大小,b表示花色)
现在要从剩下的扑克牌中再随机拿出 3 张!组成一手牌!!
其实东东除了会打代码,他业余还是一个魔法师,现在他要预言他的未来的可能性,即他将拿到的“一手牌”的可能性,我们用一个“牌型编号(一个整数,属于1到9)”来表示这手牌的牌型,那么他的未来有 9 种可能,但每种可能的方案数不一样。
现在,东东的阿戈摩托之眼没了,你需要帮他算一算 9 种牌型中,每种牌型的方案数。

输入:第 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)).
输出:输出一行,这行有 9 个整数,每个整数代表了 9 种牌型的方案数(按牌型编号从小到大的顺序)


思路:

暴力搜索

枚举剩下三张牌的所有情况,依次判断牌型。

要注意的是:首先牌型是低序号优先的,比如一对的牌就不能是同花色的或者三条或者两对的。然后,数据量很小,最多100张牌,所以复杂度可以很高。

先将5张牌按数字排序,可以方便后面的判断。

每一种的牌型判断都很简单,但容易漏条件。要多想想。

最后,要注意9中牌型的方案数要除以6,因为枚举出来的是A(3,3),但题目要求的是C(3,3)。


总结:

模测的时候排列组合了2h…

虽然注意到了数据量很小,但没想到可以直接暴力枚举…说明对数据还不够敏感。


代码:
#include <iostream>
#include <algorithm>
using namespace std;

int a,b;
int a1,b1,a2,b2;
int cnt[10];
struct C
{
	int num;
	int color;
	
	C(int x,int y){num=x;color=y;}
	bool operator != (C &c)
	{
		return num!=c.num||color!=c.color;
	}
};

void check(int i,int j,int k,int h,int p,int q)
{
	int num[]={0,a1,a2,i,k,p};	//数字 
	int color[]={0,b1,b2,j,h,q};		//花色
	sort(num+1,num+5+1);
	sort(color+1,color+5+1);
	//同花顺 
	bool f1=1,f2=1;
	for(int w=2;w<=5;w++)
	{
		if(num[w]-num[w-1]!=1)
			f1=0;
		if(color[w]!=color[w-1])
			f2=0;
	}
	if(f1&&f2)	cnt[1]++;
	//顺子 
	if(f1&&f2==0)	cnt[2]++;
	//同花 
	if(f1==0&&f2)	cnt[3]++;
	//炸弹 
	bool f4=0;
	if(num[1]==num[2]&&num[2]==num[3]&&num[3]==num[4])	f4=1;
	if(num[2]==num[3]&&num[3]==num[4]&&num[4]==num[5])	f4=1;
	if(f4&&f2==0)	cnt[4]++;
	//三带二 
	bool f5=0;
	if(num[1]==num[2]&&num[3]==num[4]&&num[4]==num[5])	f5=1;
	if(num[1]==num[2]&&num[2]==num[3]&&num[4]==num[5])	f5=1;
	if(f5&&f2==0)	cnt[5]++;
	//两对 
	bool f6=0;
	if(num[1]==num[2]&&num[2]!=num[3]&&num[3]==num[4]&&num[4]!=num[5])	f6=1;
	if(num[1]==num[2]&&num[4]==num[5]&&num[3]!=num[5]&&num[3]!=num[2])	f6=1;
	if(num[1]!=num[2]&&num[2]==num[3]&&num[3]!=num[4]&&num[4]==num[5])	f6=1;
	if(f6&&f2==0)	cnt[6]++;
	//三条 
	bool f7=0;
	if(num[1]==num[2]&&num[2]==num[3]&&num[3]!=num[4]&&num[4]!=num[5]) f7=1;
	if(num[2]==num[3]&&num[3]==num[4]&&num[1]!=num[2]&&num[4]!=num[5]) f7=1;
	if(num[3]==num[4]&&num[4]==num[5]&&num[1]!=num[2]&&num[2]!=num[3]) f7=1;
	if(f7&&f2==0)	cnt[7]++;
	//一对 
	bool f8=0;
	if(num[1]==num[2]&&num[2]!=num[3]&&num[3]!=num[4]&&num[4]!=num[5]) f8=1;
	if(num[2]==num[3]&&num[3]!=num[4]&&num[1]!=num[2]&&num[4]!=num[5]) f8=1;
	if(num[3]==num[4]&&num[4]!=num[5]&&num[1]!=num[2]&&num[2]!=num[3]) f8=1;
	if(num[4]==num[5]&&num[1]!=num[2]&&num[2]!=num[3]&&num[3]!=num[4]) f8=1;
	if(f8&&f2==0)	cnt[8]++;
	//
	if(f1||f2||f4||f5||f6||f7||f8)	cnt[9]+=0;
	else	cnt[9]++;
}
int main()
{
	cin>>a>>b;
	cin>>a1>>b1>>a2>>b2;
	C c1(a1,b1),c2(a2,b2);
	for(int i=0;i<a;i++)
	{
		for(int j=0;j<b;j++)
		{
			for(int k=0;k<a;k++)
			{
				for(int h=0;h<b;h++)
				{
					for(int p=0;p<a;p++)
					{
						for(int q=0;q<b;q++)
						{
							C c3(i,j),c4(k,h),c5(p,q);
							if(c1!=c3 && c1!=c4 && c1!=c5 && c2!=c3 && c2!=c4 && c2!=c5 && c3!=c4 && c4!=c5 && c3!=c5)
								check(i,j,k,h,p,q);
						}
					}
				}
			}
		}
	}
		for(int m=1;m<=9;m++)
		{
			cnt[m]/=6;
			if(m!=9)
				cout<<cnt[m]<<" ";
			else
				cout<<cnt[m]<<endl;
		}
	return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值