作业
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,就可以得到直径的两端点。
每个点到任意一点的最大距离可归纳为到直径两端点的最大距离。
所以分别对直径的两端点l
,r
进行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;
}