POJ1182 食物链(并查集)

1、题目解析:

在本题目中,有A,B,C三种动物,三种动物之间存在:A捕食B,B捕食C,C捕食A的奇怪捕食关系。本题目中有N([1,5000])只动物,和K([0,100000])个消息,其中存在一些消息为错误的(和之前的消息矛盾即视为错误)。题目让我们求出出错的消息的数量。

2、解题思路

首先,需要建立一个并查集,大小为动物总数的三倍。代表每一只动物都有为A动物,B动物,C动物的三种可能。并查集的代码就可以参考书上的代码。我在书上代码的基础上,改了一下变量的名字便于区分。

//并查集数组,其中前三分之一代表A动物,中间的三分之一代表B动物,最后三分之一代表C动物
int disjointSet[MAX_ANIMAL_COUNT*3];
//高度数组,用来路径压缩
int rankArrayForPathCompression[MAX_ANIMAL_COUNT*3];
//初始化并查集
void initDisjointSet() {
	for(int i=1; i<(MAX_ANIMAL_COUNT*3); i++) {
		//每个节点与自己是同一组
		disjointSet[i]=i;
		rankArrayForPathCompression[i]=0;
	}
	//初始化错误消息数量为0
	errorMessageCount=0;
}
//查询并查集的树根,顺便进行路径压缩
int findRootNodeAndPathCompression(int nodeId) {
	//当前节点就是根节点
	if(disjointSet[nodeId]==nodeId) {
		return nodeId;
	} else {
		//向上继续寻找
		return disjointSet[nodeId]=findRootNodeAndPathCompression(disjointSet[nodeId]);
	}
}
void unite(int nodeIdLeft,int nodeIdRight) {
	//寻找各自的根节点
	nodeIdLeft=findRootNodeAndPathCompression(nodeIdLeft);
	nodeIdRight=findRootNodeAndPathCompression(nodeIdRight);
	if(nodeIdLeft==nodeIdRight) {
		//本就在同一子树,则不继续合并
		return;
	}
	//不在同一子树,则需要进行合并
	if(rankArrayForPathCompression[nodeIdLeft]<rankArrayForPathCompression[nodeIdRight]) {
		disjointSet[nodeIdLeft]=nodeIdRight;
	} else {
		disjointSet[nodeIdRight]=nodeIdLeft;
		if(rankArrayForPathCompression[nodeIdLeft]==rankArrayForPathCompression[nodeIdRight]) {
			rankArrayForPathCompression[nodeIdLeft]++;
		}
	}
}
bool same(int nodeLeft,int nodeRight) {
	return findRootNodeAndPathCompression(nodeLeft)==findRootNodeAndPathCompression(nodeRight);
}

接下来,我们来分析,题目中说,输入的动物编号比动物总数还多,即为错误,因此我们写一个方法来判断它。

bool isValidInputAnimalId(int firstAnimalId,int secondAnimalId) {
	return (firstAnimalId<=animalCount)&&(secondAnimalId<=animalCount);
}

接下来,我们来分析,当输入“1 X Y”时,如何操作并查集来代表两只动物为同一类动物。我们要考虑三种情况:都为动物A,都为动物B,都为动物C。因此有了接下来的方法。

//合并两只动物为同一组
void mergeAnimalsIntoGroup(int firstAnimal,int secondAnimal) {
	//假设二者都为A
	unite(firstAnimal,secondAnimal);
	//假设二者都为B
	unite(firstAnimal+disjointSetRange,secondAnimal+disjointSetRange);
	//假设二者都为C
	unite(firstAnimal+disjointSetRange*2,secondAnimal+disjointSetRange*2);
}

再然后,我们来考虑“2 X Y”应该如何处理。不难想出,也需要考虑两只动物分别为“A B”,“B C”,“C A”时,因此有了接下来的方法。

//合并两只动物为捕食关系,前者捕食后者
void mergeAnimalEatenRelation(int huntingAnimal,int preyAnimal) {
	//假设捕食关系我A与B
	unite(huntingAnimal,preyAnimal+disjointSetRange);
	//假设捕食关系为B与C
	unite(huntingAnimal+disjointSetRange,preyAnimal+disjointSetRange*2);
	//假设捕食关系为C与A
	unite(huntingAnimal+disjointSetRange*2,preyAnimal);
}

接下来就可以分析两只动物是否为捕食关系,只需要考虑一种动物的编号和(另一只动物的编号+分组区间)即可,所以有了接下来的方法。

//是否左边的动物捕食右边的动物
bool isAnimalLeftEatAnimalRight(int animalLeft,int animalRight) {
	bool result=false;
	//假如animalLeft为A动物,animalRight为B动物
	result=result||same(animalLeft,animalRight+disjointSetRange);
	//假如animalLeft为B动物,animalRight为C动物
	result=result||same(animalLeft+disjointSetRange,animalRight+disjointSetRange*2);
	//假如animalLeft为C动物,animalRight为A动物
	result=result||same(animalLeft+disjointSetRange*2,animalRight);
	return result;
}
//判断两只动物之间是否为捕食关系
bool isEatenRelationBetweenTwoAnimals(int firstAnimal,int secondAnimal) {
	bool result=false;
	//判断是否为前者捕食后者
	result=result||isAnimalLeftEatAnimalRight(firstAnimal,secondAnimal);
	//判断是否为后者捕食前者
	result=result||isAnimalLeftEatAnimalRight(secondAnimal,firstAnimal);
	return result;
}

然后也就可以分析出如何判断两只动物是否为同一组的方法。

//判断两只动物是否为同一组
bool isTwoAnimalSameKind(int firstAnimal,int secondAnimal) {
	bool result=false;
	//两只动物都为A动物
	result=result||same(firstAnimal,secondAnimal);
	//两只动物都为B动物
	result=result||same(firstAnimal+disjointSetRange,secondAnimal+disjointSetRange);
	//两只动物都为C动物
	result=result||same(firstAnimal+disjointSetRange*2,secondAnimal+disjointSetRange*2);
	return result;
}

综上,在输入“1 X Y”时,判断是否已经存在捕食关系,如果是,则结果加一,如果不是,则合并二者为一组;在输入“2 X Y”时,判断是否已经为相反(即Y捕食X)捕食关系,是否已经存在两只动物为同一组的捕食关系,如果是则结果计数,否则合并二者的捕食关系。

三、代码

/*
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,
这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
思路:在本题目中,需要考虑的就是要找出和前面的某些真的话冲突的话.
例如,
*/
#include <iostream>
using namespace std;
//最大的动物总数
const int MAX_ANIMAL_COUNT=50009;
//最大的消息数量
const int MAX_MESSAGE_COUNT=100009;
//1代表动物之间是同类的关系
const int ANIMALS_OF_THE_SAME_KIND=1;
//0代表动物之间是捕食的关系
const int THE_FORMER_PREYS_ON_THE_LATTER=2;
//表示并查集区间, 分割三个区间,前三分之一代表A动物,中间的三分之一代表B动物,最后三分之一代表C动物
const int disjointSetRange=50000;
//动物数量
int animalCount;
//消息数量
int messageCount;
//错误消息数量
int errorMessageCount;
//并查集数组,其中前三分之一代表A动物,中间的三分之一代表B动物,最后三分之一代表C动物
int disjointSet[MAX_ANIMAL_COUNT*3];
//高度数组,用来路径压缩
int rankArrayForPathCompression[MAX_ANIMAL_COUNT*3];
//初始化并查集
void initDisjointSet() {
	for(int i=1; i<(MAX_ANIMAL_COUNT*3); i++) {
		//每个节点与自己是同一组
		disjointSet[i]=i;
		rankArrayForPathCompression[i]=0;
	}
	//初始化错误消息数量为0
	errorMessageCount=0;
}
//查询并查集的树根,顺便进行路径压缩
int findRootNodeAndPathCompression(int nodeId) {
	//当前节点就是根节点
	if(disjointSet[nodeId]==nodeId) {
		return nodeId;
	} else {
		//向上继续寻找
		return disjointSet[nodeId]=findRootNodeAndPathCompression(disjointSet[nodeId]);
	}
}
void unite(int nodeIdLeft,int nodeIdRight) {
	//寻找各自的根节点
	nodeIdLeft=findRootNodeAndPathCompression(nodeIdLeft);
	nodeIdRight=findRootNodeAndPathCompression(nodeIdRight);
	if(nodeIdLeft==nodeIdRight) {
		//本就在同一子树,则不继续合并
		return;
	}
	//不在同一子树,则需要进行合并
	if(rankArrayForPathCompression[nodeIdLeft]<rankArrayForPathCompression[nodeIdRight]) {
		disjointSet[nodeIdLeft]=nodeIdRight;
	} else {
		disjointSet[nodeIdRight]=nodeIdLeft;
		if(rankArrayForPathCompression[nodeIdLeft]==rankArrayForPathCompression[nodeIdRight]) {
			rankArrayForPathCompression[nodeIdLeft]++;
		}
	}
}
bool same(int nodeLeft,int nodeRight) {
	return findRootNodeAndPathCompression(nodeLeft)==findRootNodeAndPathCompression(nodeRight);
}
//获取输入的动物数量和消息数量
void getInputData() {
	scanf("%d%d",&animalCount,&messageCount);
}
//是否左边的动物捕食右边的动物
bool isAnimalLeftEatAnimalRight(int animalLeft,int animalRight) {
	bool result=false;
	//假如animalLeft为A动物,animalRight为B动物
	result=result||same(animalLeft,animalRight+disjointSetRange);
	//假如animalLeft为B动物,animalRight为C动物
	result=result||same(animalLeft+disjointSetRange,animalRight+disjointSetRange*2);
	//假如animalLeft为C动物,animalRight为A动物
	result=result||same(animalLeft+disjointSetRange*2,animalRight);
	return result;
}
//判断两只动物之间是否为捕食关系
bool isEatenRelationBetweenTwoAnimals(int firstAnimal,int secondAnimal) {
	bool result=false;
	//判断是否为前者捕食后者
	result=result||isAnimalLeftEatAnimalRight(firstAnimal,secondAnimal);
	//判断是否为后者捕食前者
	result=result||isAnimalLeftEatAnimalRight(secondAnimal,firstAnimal);
	return result;
}
//判断输入的动物编号是否有效
bool isValidInputAnimalId(int firstAnimalId,int secondAnimalId) {
	return (firstAnimalId<=animalCount)&&(secondAnimalId<=animalCount);
}
//合并两只动物为同一组
void mergeAnimalsIntoGroup(int firstAnimal,int secondAnimal) {
	//假设二者都为A
	unite(firstAnimal,secondAnimal);
	//假设二者都为B
	unite(firstAnimal+disjointSetRange,secondAnimal+disjointSetRange);
	//假设二者都为C
	unite(firstAnimal+disjointSetRange*2,secondAnimal+disjointSetRange*2);
}
//判断两只动物是否为同一组
bool isTwoAnimalSameKind(int firstAnimal,int secondAnimal) {
	bool result=false;
	//两只动物都为A动物
	result=result||same(firstAnimal,secondAnimal);
	//两只动物都为B动物
	result=result||same(firstAnimal+disjointSetRange,secondAnimal+disjointSetRange);
	//两只动物都为C动物
	result=result||same(firstAnimal+disjointSetRange*2,secondAnimal+disjointSetRange*2);
	return result;
}
//合并两只动物为捕食关系,前者捕食后者
void mergeAnimalEatenRelation(int huntingAnimal,int preyAnimal) {
	//假设捕食关系我A与B
	unite(huntingAnimal,preyAnimal+disjointSetRange);
	//假设捕食关系为B与C
	unite(huntingAnimal+disjointSetRange,preyAnimal+disjointSetRange*2);
	//假设捕食关系为C与A
	unite(huntingAnimal+disjointSetRange*2,preyAnimal);
}
int main() {
	//获取输入的动物数量和消息数量
	getInputData();
	//初始化并查集
	initDisjointSet();
	//输入的第一只动物
	int firstAnimal;
	//输入的第二只动物
	int secondAnimal;
	//两只动物之间的关系
	int animalRelation;
	for(int i=0; i<messageCount; i++) {
		scanf("%d%d%d",&animalRelation,&firstAnimal,&secondAnimal);
		switch(animalRelation) {
				//两种动物为同一种
			case ANIMALS_OF_THE_SAME_KIND: {
				bool isMessageCorrect=true;
				//判断输入是否有效 
				isMessageCorrect=isMessageCorrect&&isValidInputAnimalId(firstAnimal,secondAnimal);
				//判断两只动物是否有捕食关系 
				isMessageCorrect=isMessageCorrect&&(!isEatenRelationBetweenTwoAnimals(firstAnimal,secondAnimal));
				if(isMessageCorrect) {
					//如果满足两种动物为同一族群条件,则合并两只动物,记录同一族群关系
					mergeAnimalsIntoGroup(firstAnimal,secondAnimal);
				} else {
					//如果不满足条件,则输入错误,错误消息数加一
					errorMessageCount++;
				}
				break;
			}
			case THE_FORMER_PREYS_ON_THE_LATTER: {
				bool isMessageCorrect=true;
				//判断两只动物是否为同一只 
				isMessageCorrect=isMessageCorrect&&(firstAnimal!=secondAnimal);
				//判断输入是否有效 
				isMessageCorrect=isMessageCorrect&&isValidInputAnimalId(firstAnimal,secondAnimal);
				//动物二不应该能够捕食动物一 
				isMessageCorrect=isMessageCorrect&&(!isAnimalLeftEatAnimalRight(secondAnimal,firstAnimal));
				//两只动物不应该为同一种族  
				isMessageCorrect=isMessageCorrect&&(!isTwoAnimalSameKind(firstAnimal,secondAnimal));
				if(isMessageCorrect) {
					mergeAnimalEatenRelation(firstAnimal,secondAnimal);
				} else {
					errorMessageCount++;
				}
				break;
			}
			default: {
				break;
			}
		}
	}
	printf("%d\n",errorMessageCount);
	return 0;
}

四、总结

每一个小的方法只做一件事情,大的方法作为一个目录,调用小的方法,可以让逻辑更加清晰。

在编写程序过程中,很多时候会因为一个变量没有赋初值或者边界情况考虑不到,导致全盘出错,需要的是静下心调试,分析日志,来寻找原因。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值