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]

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

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 
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]

四、总结

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

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

你可能感兴趣的:(ACM,简单题目,算法,数据结构)