目录
2.[USACO3.4]美国血统 American Heritage - 洛谷
这题是很基础的并查集类型的模板题,为了方便思考,我把思路和题意分析都写在纸上了。
18号下午学长也刚好拿着题举了例,这样就更容易理解这题的做法了。
看懂题意后,可以把题目要求看作“将存在亲戚关系的人之间的关系转换成树形结构,判断询问的两个人是否在同一棵树上”,为了达成“将存在亲戚关系的人之间的关系转换成树形结构”这一任务要求,可以使“存在亲戚关系”的人的根节点为同一根节点,因此每次输入一组存在亲戚关系的人,某个树的根节点就有可能发生改变,但最终根节点的确定也标志着树形结构的完善,就类似于集合的合并完成。
明白了这点后就可以敲出能ac的代码了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int n,m,p,parent[1000100]={0};//parent[i]存储i节点的父节点
int find(int x)//查询元素根节点
{
if(parent[x] == x )//当parent[i]=i时表示该点暂时没有父节点
return x;//此时可直接返回该点,使其新增拥有父节点
else//若该点已有父节点
{
parent[x]=find(parent[x]);//向上追溯至其根节点
return parent[x];//返回根节点,这一步起到“合并”的作用
//即 ,使有亲戚关系的人的根节点最终为同一节点
}
}
void union_parent(int x,int y)
{
parent[find(x)]=find(y);//该语句表示find(x)的父节点为find(y)
}
int main()
{
scanf("%d %d %d",&n,&m,&p);
int i,a,b;
for(i=1;i<=n;i++)
parent[i]=i;
for(i=1;i<=m;i++)
{
scanf("%d %d",&a,&b);
union_parent(a,b);//将m组有亲戚关系的人建立关系
}
for(i=1;i<=p;i++)
{
scanf("%d %d",&a,&b);
if(find(a) == find(b))//判断x,y是否有同一根节点,即是否合并
printf("Yes\n");
else
printf("No\n");
}
return 0;
}
这个题和“求先序排列”一题是差不多的,搞明白了那一题,这一题也没有很大的难度,(所以我的注释也少了很多)只是注意一下,这次的输入是“中序排列”字符串和“前序排列”字符串,所以找根节点或父节点时是从首位开始找。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char s1[100],s2[100];
void dfs(int lhead,int ltail,int rhead,int rtail)//四个参数分别表示当前遍历的树的 "中序遍历范围" 以及 "前序遍历范围"
{
int i=0,j=0,k=0;
if(lhead < 0 || lhead > ltail || rhead < 0 || rhead > rtail)
return;
char z=s2[rhead];
while(s1[j] != z){
j++;
}
// printf("%d",j);
dfs(lhead,j-1,rhead+1,rhead-lhead+j);//递归遍历当前考虑节点的左子树
dfs(j+1,ltail,rhead-lhead+j+1,rtail);//递归遍历当前考虑节点的右子树
printf("%c",s1[j]);
}
int main()
{
scanf("%s",s1);//中序遍历
scanf("%s",s2);//前序遍历
int i,j,ls1;
ls1=strlen(s1)-1;
dfs(0,ls1,0,ls1);
return 0;
}
这道题就是一道很基础的二叉树的题,刚好可以让我加深一下对“树”的理解。
题目会输入n组数据,每组数据都有三个字符,依次表示当前节点(父节点)、当前节点的左子节点以及右子节点。
因此,我设立了一个结构体数组,包含两个结构体指针变量以及一个char型成员变量。随后就要对有效区间内的结构体数组元素进行初始化。
然后就是常规的输入输出操作了,这里,为了方便,我设了一个传递指针参数的输出函数。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define f (sizeof(struct abc))
int n;
struct abc
{
char data;
struct abc * left;
struct abc * right;
}tree[10000];
struct abc * head;
void printf_tree(struct abc * k)
{
printf("%c",k->data);
if(k->left != NULL)//判断是否有左节点
printf_tree(k->left);
if(k->right != NULL)//判断是否有右节点
printf_tree(k->right);
}
int main()
{
scanf("%d",&n);
getchar();
int i,j,k1;
for(i=0;i<=n;i++)
{
tree[i].left=NULL;
tree[i].right=NULL;
tree[i].data=i;
}
for(i=1;i<=n;i++)
{
char s[4];
scanf("%s",s);//s[0]为父节点,s[1],s[2]分别为其左右子节点,s[3]用来吃掉多余的'\n'缓冲字符
if(i == 1)
head=&tree[s[0]];
tree[s[0]].data=s[0];
tree[s[1]].data=s[1];
tree[s[2]].data=s[2];
if(s[1] != '*')
tree[s[0]].left=&tree[s[1]];
if(s[2] != '*')
tree[s[0]].right=&tree[s[2]];
}
printf_tree(head);
return 0;
}
题目描述很精简,短短几行而已。要求很明确,根据输入的两串分别按中序排列、后序排列的字符串明确树的结构,再通过“前序排列”将其输出
根据中序排列规则是“左->根->右”,后序排列规则是“左->右->根”,可以推知,后序排列的末尾元素就是整棵树的根节点,从后往前推,每个元素都是上个节点的右子树的父节点。
以题目给出的测试样例为例,那么可以找出根节点是A,这时再考虑中序排列的规律,可以推知,根节点的左侧元素为它的左子树节点,而右侧元素则为右子树节点。
想到这里,那么就会很自然地想到,其它子节点都可以这么推导然后得到它的左右子节点,那么题目就转换成了一个dfs类型的题目。
这里将中序排列与后序排列的范围区间作为参数,注意递归时传递的参数需要按“当前考虑的树”来判断,所以递归时要找出原树与递归子树的区间关系,根据这个来传递参数。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char s1[10],s2[10];
void build(int lhead,int ltail,int rhead,int rtail)//当前考虑的树
//中序遍历范围为[lhead,ltail],后序遍历范围为【rhead,rtail】
{
int i,j,k,p=lhead;
if( lhead < 0|| rhead < 0 ) //遍历发现不合法数据,返回
return;
if( lhead > ltail || rhead > rtail ) //遍历发现为空树时返回
return;
printf("%c",s2[rtail]); //后序遍历末位元素为当前考虑的树的根节点,直接输出
while(s1[p] != s2[rtail]) //在中序遍历字符串中查找
p++; //从中序遍历中找出左树的范围
k=p-lhead; //计算左子树节点个数
build(lhead,p-1,rhead,rhead+k-1);//递归判断当前节点的左子树
//其中序范围为[lhead,p-1]后序范围为[rhead,rhead+k-1]
build(p+1,ltail,rhead+k,rtail-1); //递归当前节点的右子树,同上
}
int main()
{
scanf("%s",s1);
scanf("%s",s2);
int length;
length=strlen(s1)-1;
build(0,length,0,length);
}
相信大家看到这道题后第一想法是写出一个类似与这样的代码
当然咯,大家肯定会多多少少加些东西在这里,例如左右指针指向左右子节点啥的,其实这样的想法没啥毛病,但是可能问题就出在“用指针将各个节点连接起来后不知道怎么遍历输出”,然后就自然而然地简简单单地用一个循环去遍历、输出,结果你就发现其实代码主要使用的部分只有上图那样一部分,而你拿上面的代码一交居然也能有80分。
而上面的代码怎么错了呢?很明显的,倘若相邻序号的两个节点的子节点没有重合部分就可能出错了,不信,你拿这组样例试一试:
7
4 6
7 3
0 0
0 2
0 0
0 5
0 0
按上图代码得到的是“3”,然而实际应该是“4”
那么重新再理解一下题意,就能明白过来,这题估计是得用dfs。明确方向后就可以敲代码了
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
int n,ans=0;
struct abc{
struct abc * left;
struct abc * right;
}tree[100010];
int max(int x,int y){
return (x>y?x:y);
}
void dfs(struct abc * x,int deep){
ans=max(ans,deep);
if(x->left !=NULL)
dfs(x->left,deep+1);
if(x->right != NULL)
dfs(x->right,deep+1);
}
int main()
{
int j,i;
struct abc * head;
scanf("%d",&n);
for(i=0;i<=n;i++){
tree[i].left =NULL;
tree[i].right=NULL;
}
for(i=1;i<=n;i++){//循环遍历第i个节点情况
int a,b;
scanf("%d %d",&a,&b);
if(a != 0)
tree[i].left =&tree[a];
if(b != 0)
tree[i].right=&tree[b];
if(i == 1)
head=&tree[i];
}
dfs(head,1);
printf("%d",ans);
return 0;
}
在中序遍历、后序遍历以及前序遍历三种遍历方式中,知道任意两种遍历得到的字符串都可以推知原树的模样,除了“知道前序和后序遍历的字符串”的情况,具体为什么,我们可以自己来尝试推一下:
先尝试一下复杂一些的树的遍历,这里还看不太出来其中的规律,主要目的是要确保自己真的明白三种遍历方式是怎么进行的。
接下来再看看简单树的遍历情况:
从这里我们可以明白,当确定前序遍历和后序遍历得到的字符串时,得到的树确实尚不确定,那么怎么确定凭当前已知的前序后序遍历的字符串可以找出几种不同的树呢?
继续思考,我们发现某个节点只有一个叶节点时,叶节点在左节点上还是在右节点上,它的前序遍历和后序遍历得到的字符串都是一样的,但是它的中序遍历结构却不一样。进一步思考,我们发现不仅仅是叶节点,当某个节点只有左子树或右子树时情况也是一样的。
而当只有一个子树时,其子树上的唯一节点仍需考虑左右情况。
至此,我们发现规律:可得到树的个数=二的“单一节点数”次方
(图来自《大话数据结构》)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
char s1[100010],s2[100010];
int main()
{
int i,j,k,ans=0;
scanf("%s",s1);
scanf("%s",s2);
int l1=strlen(s1),l2=strlen(s2);
for(i=0;i<l1;i++)//在前序字符串中不在首位末位
for(j=1;j<l2;j++)//在后序字符串中不在首位末位
if(s1[i] == s2[j] && s1[i+1] == s2[j-1])//在两字符串中字符相同且顺序相反
ans++;//则叶节点+1
k=pow(2,ans);
printf("%d",k);
return 0;
}
这道题可以说是学习“并查集”的一道很好的入门题。利用循环遍历将所有存在联系的节点的根节点统一为同一个节点,不同的根节点就表示不同的集合,这样就完成了“合并”的任务,输出时判断需询问的两个数值的节点的根节点是否相同就好了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int parent[1000010];
int n,m,z;
int find(int x)//找到根节点
{
if(parent[x] != x)
parent[x]=find(parent[x]);
return parent[x];
}
void union_parent(int x,int y)//统一父节点,即合并集合
{
parent[(find(x))]=find(y);
}
int main()
{
int i,j;
scanf("%d %d",&n,&m);
for(i=0;i<=n;i++)
parent[i]=i;
for(i=1;i<=m;i++)
{
int a,b;
scanf("%d %d %d",&z,&a,&b);
if(z == 1)
{
union_parent(a,b);
}
if(z == 2)
{
if(find(a) == find(b))
printf("Y\n");
else
printf("N\n");
}
}
return 0;
}
按题意来说,就是要将有关系的云朵打包成一个集合,最后在当前有限的钱数下尽可能买到最高价值的云朵集合。
简单说来就是“并查集”+“01背包”。
在这题里,可以将集合个数看作“可以放进背包的物品个数”,将集合价值看作“物品价值”,弄明白这些后就可以开始敲代码了
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int n,m,w,parent[100010],dp[100010];
int max (int x,int y)
{
return (x>y?x:y);
}
struct abc
{
int value;
int price;
}tree[100010];
int find(int x)//查找根节点
{
if(parent[x] != x )
parent[x]=find(parent[x]);
return parent[x];
}
void union_parent(int x,int y)//将两节点建立联系,即“合并 ”
{
if( find(x) != find(y) )
parent[find(x)]=find(y);//该语句表示find(x)的父节点为find(y)
}
int main()
{
scanf("%d %d %d",&n,&m,&w);
int i,j,a,b,v,ans=-9999;
for(i=1;i<=n;i++){
parent[i]=i;
scanf("%d %d",&tree[i].price,&tree[i].value);}
for(i=1;i<=m;i++){
scanf("%d %d",&a,&b);
union_parent(a,b);//建立关系
}
for(i=1;i<=n;i++){
if(parent[i] != i) {/*不是根的话,把同一个集合的price和value累加到根的那个数组 */
tree[find(i)].price += tree[i].price; //累加到根的那个数组
tree[find(i)].value += tree[i].value;
tree[i].price=0; //加过就更新为0,避免之后遍历到时再次加入
tree[i].value=0;
}
}
for(i=1;i<=n;i++) //01背包问题:先循环遍历所有物品(所有云朵集合)
for(v=w;v>=tree[i].price;v--)
dp[v] = max(dp[v],dp[v-tree[i].price]+tree[i].value);
printf("%d",dp[w]);
return 0;
}
已知有A,B两家公司,分别有员工n,m,人,在A公司的n个人中存在p对好友关系,在B公司的m个人中存在q对好友关系,同时小明小红编号分别为1,-1,且两者为情侣关系。
A公司都是男性,编号为正,B公司都是女性,编号为负。
要求输出包括小明小红在内,小明小红最多能促成几对情侣出现。
(实际上我感觉题目表述有些问题来着)
简单说来就是求“与小明认识的人”和“与小红认识的人”两个数值之间的较小数(明明两句话就可以说完的题硬是说成我一时半会没看明白的样子)
很好解决,开一个足够大的数组,要能同时装下两家公司的所有员工,然后分别“ 合并 ”,计算“与小明认识的人”和“与小红认识的人”两个数值后输出较小值就好了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int n,m,p,q,parent[100010],ans=0,ans1=0,ans2=0;
int min(int x,int y)
{
return (x<y?x:y);
}
void union_parent(int x,int y)
{
if(find(x) != find(y))
parent[find(x)]=find(y);
}
int find(int x)
{
if(parent[x] != x)
parent[x]=find(parent[x]);
return parent[x];
}
int main()
{
scanf("%d %d %d %d",&n,&m,&p,&q);
int i,j,a,b;
for(i=0;i<=n+m;i++)
parent[i]=i;
for(i=1;i<=p;i++){
scanf("%d %d",&a,&b);
union_parent(a,b);
}
for(i=1;i<=q;i++){
scanf("%d %d",&a,&b);
a*=-1;
b*=-1;
union_parent(a+n,b+n);
}
for(i=1;i<=n;i++)
if(find(i) == find(1))
ans1++;
for(i=n+1;i<=n+m;i++)
if(find(i) == find(n+1))
ans2++;
ans=min(ans1,ans2);
printf("%d",ans);
return 0;
}
已知有n个村庄,m条将要打通的道路,知道每条道路需要打通将花费的时间。输出所有村庄连通时的最短时间。
既然要求最短时间,那么我设立了一个快排函数,用于按时间长短来给每条道路排序。
我设了一个结构体变量,用来存储每组道路数据,然后经过排序后在拿出来一 一连通,同时循环判断是否所有村庄都连通了,(就是一个简单的嵌套循环)若是,则直接输出当前遍历到的道路所需时间并终止程序运行,反之,若循环完全结束后程序仍在执行则输出“-1”,表示“所有道路打通后也无法做到连通所有村庄”
这里注意一下,直接嵌套容易时间超限,所以我们对于内部的嵌套的循环只要取到n/2就行了,也足够判断所有村庄了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int parent[100010],n,m;
struct abc
{
int x,y,t;
}a[100010],t1;
int find(int x)
{
if(parent[x] != x)
parent[x]=find(parent[x]);
return parent[x];
}
void union_parent(int x,int y)
{
if(find(x) != find(y))
parent[find(x)]=find(y);
}
void quicksort(int left,int right)
{
int i,j,temp;
if(left > right)
return ;
i=left;
j=right;
temp=a[(left+right)/2].t;
while(i <= j){
while( a[j].t > temp )
j--;
while( a[i].t < temp )
i++;
if( i <= j ){
t1=a[i];
a[i]=a[j];
a[j]=t1;
i++;
j--;
}
}
if(left < j) quicksort(left,j);
if(i < right) quicksort(i,right);
return;
}
int main()
{
scanf("%d %d",&n,&m);
int i,j,k,flag=0;
for(i=0;i<=n;i++)
parent[i]=i;
for(i=1;i<=m;i++)
scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].t);
quicksort(1,m);
for(i=1;i<=m;i++)
{
flag=0;
union_parent(a[i].x,a[i].y);
for(j=1;j<n/2;j++){
if(find(j) != find(n-j+1)){
flag=-1;
break;}
}
if( flag == 0 ){
printf("%d",a[i].t);
exit(0);}
}
printf("-1");
return 0;
}
普通的并查集是双向连接的,就好比你把1、2,1,3连接后find(2)和find(3)的结果是一样的,但在这题要求的应是单项连接,当然,也好解决,用数组将各个愿意分享光盘的人用下标表示、存储起来,用0,1表示愿意分享光盘与否,逐个判断,明确“ 若i愿意给j,j愿意给k,那么可视为i愿意给k ”这一点就可以做出来了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int parent[100010],link[1000][1000];
int n,ans;
int main()
{
int i,j,k;
scanf("%d",&n);
for(i=0;i<=n;i++)
parent[i]=i;//初始化
for(i=1;i<=n;i++){
while(scanf("%d",&k) && k){//循环输入第i个人愿意分享的人的编号,若为0则停止本次输入
link[i][k]=1;
}
}
for(i=1;i<=n;i++){//从第一个人开始遍历
for(j=1;j<=n;j++){//判断第i个人是否愿意分享给第j个人
for(k=1;k<=n;k++){//同时判断第j个人是否愿意分享给第k个人
if(link[i][j] == 1 && link[j][k] == 1)//若i愿意给j,j愿意给k,那么可视为i愿意给k
link[i][k]=1;
}
}
}
for(i=1;i<=n;i++){
for(j=1;j<=n;j++){
if(link[i][j] == 1)//若第i个人愿意给第j个人
parent[j]=parent[i];//则令第j个人的父节点为第i个人
}
}
for(i=1;i<=n;i++) {
if(parent[i] == i)
ans++; //若i的父节点就是自己本身的话,最后需要的光盘数+1
}
printf("%d",ans);
return 0;
}
由于这题是学校网站上的,不是我们学校的可能看不到,我就把题目复制一下:
题目描述
给定一个序列,按先序序列建立二叉树。输出建立后的二叉树的从上到下层次遍历序列。
输入格式
一个序列
输出格式
从上之下层次序列
样例输入
ABC##DE#G##F###
样例输出
ABCDEFG
很基础的一道题,但也刚好是我当前阶段需要的。
首先设立一个结果体,包含三个成员变量:输入的数据、左指针、右指针。
随后就是输入部分,这里我是选择像建立链表一样的方式开辟新的子树节点,使用递归的方式一个一个节点输入。
输出部分,我是用的类似于队列的操作,设两个整型数值分别指向头尾,利用指针逐个将节点压入结构体数组中,完成到这一步就可以输出了。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define f (sizeof(struct abc))
int parent[100010],n,m;
struct abc {
char data;
struct abc * left ;
struct abc * right;
};
struct abc * t[100010];
struct abc * creat(){
char ch;
struct abc * tree;
scanf("%c",&ch);
if(ch == '#')
tree=NULL;//若为 ‘# ’,则表示当前子树已遍历到叶节点,返回
else{
tree=(struct abc *) malloc (f);
if( tree == NULL )
exit(0);
tree->data = ch;
tree->left = creat();
tree->right = creat();
}
return tree;
}
void printf_tree(struct abc * x)
{
if(x != NULL)
t[0]=x;
int i,head=1,number=0;
while(1)
{
//t[ number ]的左右子结点依次存入t中
if( t[ number ] -> left != NULL )
t[ head ++ ] = t[ number ] -> left;
if( t[ number ] -> right != NULL )
t[ head ++ ] = t[ number ] -> right;
number ++;
if( number == head )
{
//当number == head为真,表示所有结点均已存入t数组
t[ number ] = NULL;
break;
}
}
number = 0;
while( t[ number ] )
{
//层序输出二叉树结点
printf("%c",t[ number ] -> data);
number ++;
}
}
int main(){
struct abc * k;
int i;
k=creat();
printf_tree(k);
}