1.并查集
并查集是一种用于处理不交集的合并和查询的数据结构。并查集有两个功能,1)判断一个元素是否属于同一个集合。2)按某种要求合并不同的集合。
例如现在有集合A{1,2,3},B{5,6,7},C{8,0}。
1,2属于同一集合。1,5不属于同一集合。
合并A,B集合就变成了{1,2,3,5,6,7},{8,0}。
并查集将每一个集合看作一棵树。每个并查集选出其中的一个元素作为根。
查询的时候从当前结点开始,回溯到该并查集对应的根。
合并的时候将某一个集合对应的根节点作为另一棵树的子树进行合并,合并成一棵更大的树。
进行模拟的时候一般用两个数组来进行模拟,一个parent,用于记录各结点的父节点,另一个是height,用于记录以该结点为根的子树的高度。初始化parent[i] = i; height[i] = 0;
/*
注释:根节点具有的性质:x_root = parent[x_root]。
如果不是向上回溯:x_root = parent[x_root]。
*/
int find_root(int x){
int x_root = x;
while(x_root != parent[x_root]{ //不是根节点根节点
x_root = parent[x_root];
}
return x_root;
}
void union_set(int x,int y){
x = find_root(x);
y = find_root(y);
if(height[x] < height[y]){
//将x作为y的子树合并。
parent[x] = y;
}else if(height[y] < height[x]){
//将y作为x的子树合并。
parent[y] = x;
}else{
//假设将x作为y的子树合并。
parent[x] = y;
height[y]++;
}
}
完成union操作后,对parent数组进行遍历,parent数组中parent[i] = i的元素个数就是合并后的集合的个数。
并查集的作用:
1.判断一个图中是否有环。
2.判断一个图中连通分量的个数。
3.假设每个结点最多只有一个孩子,求某个结点的祖先结点和后代节点。
例题1.找出直系亲属
思路:可以建立children数组用于记录每个结点对应的孩子结点。建立parent后,如要判断某两个结点A,B之间的亲属关系。先假设结点A是结点B祖先,我们不停地做A = parent[A]的迭代操作,看看有没有A == B的可能。但当A已经回溯到该并查集的根节点(也就是满足当前A == parent[A])的时候,且A !=B,就证明:结点A并不是结点B的祖先。如果A不是B的祖先,就反过来判断结点B是否为A的祖先。如果也不是,那么两者之间就没有关系。
#include <iostream>
using namespace std;
const int MAXN = 26;
int children[MAXN];
bool visit[MAXN]; //用于记录拥有哪些结点。
void Initialize(){ //初始化children和visit数组
for(int i=0;i<MAXN;i++){
children[i] = i;
visit[i] = false;
}
}
int main() {
Initialize(); //初始化
/*
输入n,m。n是关系数量,m是要判断的关系数量。
*/
int n,m;
scanf("%d%d",&n,&m);
getchar();
string input;
for(int i=0;i<n;i++){
/*
根据输入更新children和visit数组
*/
getline(cin,input);
int child = (int)(input[0]-'A');
if(input[1] != '-'){
int parent1 = (int)(input[1]-'A');
visit[parent1] = true;
children[parent1] = child;
}
if(input[2] != '-'){
int parent2 = (int)(input[2]-'A');
visit[parent2] = true;
children[parent2] = child;
}
visit[child] = true;
}
char node1,node2;
int node1Index,node2Index;
for(int i=0;i<m;i++){
bool flag = true; //是否有直系关系
bool pa = true; //true-->是直系亲属,是长辈。
int level = 0; //相差辈数
cin>>node1>>node2; //输入要判断的两结点关系。
/*
判断node1是否为node2的祖先。
*/
node1Index = (int)(node1-'A');
node2Index = (int)(node2-'A');
while(node1Index!=node2Index){
if(node1Index == children[node1Index]){
flag = false;
break;
}else{
node1Index = children[node1Index];
level += 1;
}
}
/*
如果不是,尝试判断node1是否为node2的后代。即:
node2是否为node1的孩子。
*/
node1Index = (int)(node1-'A');
node2Index = (int)(node2-'A');
if(flag == false){
flag = true;
level = 0;
while(node2Index!=node1Index){
if(node2Index == children[node2Index]){
flag = false;
break;
}else{
node2Index = children[node2Index];
level += 1;
pa = false;
}
}
}
/*
输出结果
*/
if(flag == false){ //如果没有关系
cout<<"-"<<endl;
}else if (pa == true){ //如果node1为node2的祖先
if(level == 1){
cout<<"parent"<<endl;
}else if(level == 2){
cout<<"grandparent"<<endl;
}else{
for(int k=0;k<level-2;k++){
cout<<"great-";
}
cout<<"grandparent"<<endl;
}
}else{ //如果node1为node2的孩子
if(level == 1){
cout<<"child"<<endl;
}else if(level == 2){
cout<<"grandchild"<<endl;
}else{
for(int k=0;k<level-2;k++){
cout<<"great-";
}
cout<<"grandchild"<<endl;
}
}
}
return 0;
}
例题2.找出帮派和头目
思路:
1.建立Person结构体,包含姓名、该人的通话时长、以及一个int类型的变量branches,该变量只在该Person结点作为其所在帮派对应的并查集的根节点时有效,用于记录该并查集中所含的结点数量。
2.利用map<int,Person>,建立Person和int的映射。转化为用int表示一个人。一个int能唯一地确定一个人。
3.稍加改动union_set(),算出该图中有几个联通分支。
这里说的稍加改动是说:parent数组中的元素最终只包含并查集的根节点。
例如:该图中一共有四个连通分量,对应四个并查集,分别以1,3,8,9为根。
则改进后的parent数组可以是[1,1,1,3,3,3,8,3,8,9],总之parent数组中只能含有1,3,8,9这四个数。这样做的好处是能够很明确地标识哪几个人属于同一个帮派和统计可能存在的帮派数。
4.求出1,3,8,9这几个数之后,1,3,8,9中的每一个数代表了一个可能存在的帮派。将这几个数入队列q。
5.当q非空时,q中头元素出队列,用currentNode记录该头元素,遍历parent数组,看看有多少个和currentNode一样的元素,用nodeNum记录一下。遍历过程中用maxNodeIndex记录当前遍历的结点的下标,选出对应Person结点中minues最大的结点,用maxNodeIndex记录,遍历过程中还应用totalValue记录该连通分支中的所有Person结点的minutes和。当totalValue > 2 * K就证明权重大于给定的威胁K。
6.建立一个优先队列,result,如果nodeNum>=3且totalValue > 2 * k,就证明存在一个帮派,该帮派中的帮主已经由maxNodeIndex记录,可以将以maxNodeIndex为下标的Person结点进入这个优先队列。之所以要选用优先队列,是因为题目中要求各帮主的输出顺序要按照字母表顺序,通过在Person类中重载这一功能可以实现这一要求。
7.重复上述操作5,6,直到q为空。
8.此时,依次从优先队列result中取出元素按要求输出即可。
#include <iostream>
#include <map>
#include <queue>
using namespace std;
const int MAXN = 1000;
int parent[MAXN];
int height[MAXN];
struct Person{
string name;
int minutes; //与之关联的通话分钟数。
int branches; //当该结点为某并查集的根节点时,branches位有效,标识该连通分量所含的结点数
Person(string n):name(n),minutes(0),branches(0){};
bool operator< (Person p) const{ //重载‘<’运算符,用于优先队列排序。
return (int)(name[0]) > (int)(p.name[0]);
}
};
void Initialize(){
for(int i=0;i<MAXN;i++){
parent[i] = i;
height[i] = 0;
}
}
int find_root(int x){
int x_root = x;
while(x_root != parent[x_root]){
x_root = parent[x_root];
}
return x_root;
}
void union_set(int x,int y){
/*
这里的合并并查集操作与传统方法有所改动。
例如并查集A{5,8}(根结点为5),B{1,3}(根节点为1),将B作为A的子树合并时,需要修改所有parent[i] = 1的结点而不是只修改i = 1的结点。
*/
int x_root = find_root(x);
int y_root = find_root(y);
if(height[x_root] < height[y_root]){
for(int i=0;i<MAXN;i++){
if(parent[i] == x_root){
parent[i] = y_root;
}
}
}else if(height[y_root] < height[x_root]){
for(int i=0;i<MAXN;i++){
if(parent[i] == y_root){
parent[i] = x_root;
}
}
}else{
for(int i=0;i<MAXN;i++){
if(parent[i] == x_root){
parent[i] = y_root;
}
}
height[y_root]++;
}
}
int findExists(string name,map<int,Person> myMap){
/*
添加节点时,如果已经在map中,不添加新的结点。
*/
map<int,Person>::iterator it; //迭代器
for(it = myMap.begin();it !=myMap.end();it++){
if(it->second.name == name){
return it->first;
}
}
return -1;
}
map<int,Person> myMap;
int main() {
int N,K; //N个关系,K给定的威胁
while(cin>>N>>K){
Initialize(); //初始化
int nodeNum = 0; //人的总数
getchar();
/*
输入N个关系,添加各个Person节点的name,minutes信息到map中
*/
string name1,name2;
int nameindex1,nameindex2;
int length;
for(int i=0;i<N;i++){
cin>>name1>>name2>>length;
if(findExists(name1,myMap) == -1){
nameindex1 = nodeNum;
myMap.insert(pair<int,Person>(nodeNum,Person(name1)));
nodeNum++;
}else{
nameindex1 = findExists(name1,myMap);
}
myMap.at(findExists(name1,myMap)).minutes += length;
if(findExists(name2,myMap) == -1){
nameindex2 = nodeNum;
myMap.insert(pair<int,Person>(nodeNum, Person(name2)));
nodeNum++;
}else{
nameindex2 = findExists(name2,myMap);
}
myMap.at(findExists(name2,myMap)).minutes += length;
union_set(nameindex1,nameindex2);
}
queue<int> q; //用于记录可能是的帮派并查集根节点
priority_queue<Person> result; //优先队列
for(int i=0;i<nodeNum;i++){
if(parent[i] == i){
q.push(i); //可能是的帮派,入q这个队列
}
}
int currentNode;
while(!q.empty()){
int nodeCount = 0; //计算某帮派中的成员总数
int maxNode = -1; //用于计算总权重最大的个体
int totalValue = 0; //计算总关系权重
int maxNodeIndex; //记录一个帮派中的帮主编号
currentNode = q.front();
q.pop();
int i=0;
for(;i<nodeNum;i++){
if(parent[i] == currentNode){
nodeCount++;
if(maxNode < myMap.at(i).minutes){
maxNode = myMap.at(i).minutes;
maxNodeIndex = i;
}
totalValue += myMap.at(i).minutes;
}
}
myMap.at(maxNodeIndex).branches = nodeCount;
if((nodeCount >= 3)&&(totalValue > 2*K)){ //超过两个人且总关系权重大于给定的威胁K
result.push(myMap.at(maxNodeIndex));
}
}
/*
输出结果
*/
cout<<result.size()<<endl;
while(!result.empty()){
cout<<result.top().name<<" ";
cout<<result.top().branches;
result.pop();
cout<<endl;
}
}
return 0;
}
2.打家劫舍
假设你是一个专业的小偷,盗窃沿街的房屋。沿街房屋中的财产价值由一个数组nums给定,例如nums = [1,2,3,1]。要求你求出你能偷的最大金额,前提是不能连续偷窃两间相邻的房屋。
思路:考虑子问题:从第i间房屋开始打劫所能获取的最大金额。
定义一个record[nums.length+1]的数组,record[i]表示从第i间房屋开始打劫所能获取的最大金额。其中规定record[nums.length] = 0;
则record[nums.length-1] = nums[nums.length-1];
除此之外,record[i] = max{nums[i] + record[i+2],record[i+1]}。(即盗窃下标为i的房屋的情况和不盗窃下标为i的房屋的情况中的最大值)
求出的record[0]即为所求。
java代码实现:
class Solution {
int record[];
public int rob(int[] nums) {
return robchild(0,nums);
}
public int robchild(int num,int[] nums){
/*
从index开始打家劫舍
*/
int length = nums.length;
record = new int[length+1];
for(int index=length;index>=0;index--){
if(index>=length){
record[index] = 0;
}else{
if(index == length-1){
record[index] = nums[index];
}else{
record[index] = Math.max(nums[index] + record[index+2],record[index+1]);
}
}
}
return record[num];
}
}