数据结构与算法

目录

并查集 

Kruskal算法+并查集

模板题

无穷的宗教(poj 2524)

 嫌疑人(poj 1611)

 经典题:

食物链:(poj 1182)

贪心算法 

区间问题

今年暑假不AC(hdu 2037)

最少硬币问题 

最少硬币数量

动态规划

基本dp


并查集 

下面会以Kruskal算法引出并查集的基本模板

Kruskal算法+并查集

对于稀疏图来说,用Kruskal写最小生成树效率更好,加上并查集,可对其进行优化。

Kruskal算法的步骤:

用U表示已经使用过的点的集合,V表示还未使用过的点的集合

1.对所有边进行从小到大的排序。

2.每次选一条边(最小的边),如果形成环则不加入U中,否则加入。那U一定是最佳的。

并查集:
我们可以把每个连通分量看成一个集合,该集合包含了连通分量的所有点(一个集合只有一个首领,在并查集里面称之为“代表元”)。而具体的连通方式无关紧要,好比集合中的元素没有先后顺序之分,只有“属于”与“不属于”的区别。图的所有连通分量可以用若干个不相交集合来表示。

而并查集的精妙之处在于用数来表示集合。如果把x的父结点保存在p[x]中(可以理解为x的上级)

并查集可以分为三步走:

  • 初始化p数组:先给p数组的父节点定为自身(单个也属于集合)

int init(int n)
{
    for(int i = 0 ; i < n ; i++)
    {
        pre[i] = i;
    }
}

  • 寻找它的首领(在树里面称之为:根节点):

 int find(int x)
{
    if(pre[x] == x ) return x;
    return pre[x] = find(pre[x]);
}

 解释: pre[x] = find(pre[x]);

这里采用了路径压缩的方法,如果集合成左侧的线性(一条线一样)从底层访问跟(以d为根),会浪费大量的时间,所以把它变为直属关系(即我的上级就是首领)大大节约时间

如果p[x]=x,说明x本身就是树根,因此返回x;否则返回x的父亲p[x]的上一级,直到找到首领(d)为止。然每棵树表示的只是一个集合,因此树的形态是无关紧要的,并不需要在“查找”操作之后保持树的形态不变,只要顺便把遍历过的结点都改成树根的儿子,下次查找就会快很多了

  • 合并集合

bool join(int x , int y)
{
    x = find(x);
    y = find(y);
    if(x == y ) return false; // 处于同一集合 

   else

   pre[y] = x; // 也可以写成pre[x] = y;
    return true;
}  

解释  if(x == y ) return false;

拿上面的2 和 3 为例

假设 x = 2 ,y  = 3 时, x , y 的首领都相同都是 1 如果将 x,y加入集合将会形成一个环(链接2-3)否则如图所示加入集合(以1为首领,这个首领你也可以以4,但为了两边的高度差不大,尽力以深度较高的作为首领,感兴趣的可以看看这个篇博客:并查集(Kruskal算法求最小生成树中判断是否会出现环) · 语雀

  •  完整代码的实现:

解释 :

下面的rank数组用来记录集合的深度,即上面提到的为使合并后的集合两边高度相差不大,对高度进行比较,读者可以画个图,对照一下(深一点的作为首领,一样深两者都可)

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1005;
int pre[N] , rank[N] ;
typedef struct
{
	int f;
	int e;
	int v;
}k;

bool cmp(k a, k b)
{
	return a.v < b.v;
}
// 初始化
int init(int n)
{
	for(int i = 0 ; i < n ; i++)
	{
		pre[i] = i;
		rank[i] = 1;
	}
}

// 查找 

int find(int x)
{
	if(pre[x] == x ) return x;
	return pre[x] = find(pre[x]);
}
// 判断两个节点是否联通
bool issame(int x , int y)
{
	return find(x) == find(y);
} 
// 合并
bool join(int x , int y)
{
	x = find(x);
	y = find(y);
	if(x == y ) return false;
	if(rank[x] > rank[y]) pre[y] = x;
	else
	{
		if(rank[x] == rank[y]) rank[y]++;
		pre[x] = y;
	}
	return true;
} 
int main()
{
	k k_1[N];
	int ans = 0;
	cout << "请输入你的边数:" << endl;
	int n;
	cin >> n;
	for(int i = 0 ; i < n ; i++)
	{
		cin >> k_1[i].f >> k_1[i].e >> k_1[i].v;
	}
	sort(k_1,k_1+n,cmp);
	init(n);
	// 寻找最路径
	for(int i = 0 ; i < n ; i++)
	{
		if(join(k_1[i].f , k_1[i].e)) 
		{
			ans += k_1[i].v;
		}
	}
	cout << ans << endl;
	return 0; 
} 

模板题


无穷的宗教(poj 2524)

 当今世界有很多不同的宗教,很难通晓他们。你有兴趣找出在你的大学里有多少种不同的宗教信仰。
你知道在你的大学里有n个学生(0 < n <= 50000) 。你无法询问每个学生的宗教信仰。此外,许多学生不想说出他们的信仰。避免这些问题的一个方法是问m(0 <= m <= n(n - 1)/ 2)对学生, 问他们是否信仰相同的宗教( 例如他们可能知道他们两个是否去了相同的教堂) 。在这个数据中,你可能不知道每个人信仰的宗教,但你可以知道校园里最多可能有多少个不同的宗教。假定每个学生最多信仰一个宗教。
Input
有多组数据。对于每组数据:
第一行:两个整数n和m。
以下m行:每行包含两个整数i和j,表示学生i和j信仰相同的宗教。学生编号从1到n。
输入的最后一行中,n = m = 0结束
Output
对于每组测试数据,输出一行,输出数据序号( 从1开始) 和大学里不同宗教的最大数量。(参见样例)

输入 

10 9
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
10 4
2 3
4 5
4 8
5 8
0 0

输出 

Case 1: 1
Case 2: 7

思路:不同的宗教相当于一个子集的根节点,所以只要统计有多少个子集就可以的出有多少的宗教 ,只要遍历在pre中有多少个父节点没有改变的个数就是其连通子集的个数

其中(b)的连通子集个数是:A,B,D,E 四个,(c)中有A,B,D三个 

//代码实现
​
for(int i = 1 ; i <= n ; i++)
{
    if(pre[i] == i)
       ans++;
}
 
​

 完整代码:

#include<iostream>
#include<cstdio>
using namespace std;
int n , m;
int pre[50010];
 
// 初始化 
void inint()
{
	for(int i = 1 ; i <= n ; i++)
	{
		pre[i] = i;
	}
}
//查找父节点
int find(int x)
{
	if(x == pre[x]) 
	return x;
	else
	return pre[x] = find(pre[x]);
} 
 
// 合并
int unio(int x, int y)
{
	x = find(x);
	y = find(y);
	if(x == y ) return false;
	else
	pre[x] = y;
} 
int main()
{
	int ans = 0;
	int sum = 1;
	while(scanf("%d%d",&n,&m) == 2 && n != 0 && m != 0)
	{
		int i , j;
		inint();
		ans = 0;
		while(m--)
		{
		scanf("%d%d",&i,&j);
		
		unio(i,j);	
		}
		for(int i = 1 ; i <= n ; i++)
		{
			if(pre[i] == i)
			ans++;
		}
		printf("Case %d: %d\n",sum++,ans);
	}
	return 0;
} 


 嫌疑人(poj 1611)

描述

严重急性呼吸系统综合征(SARS)是一种病因不明的非典型肺炎,于2003年3月中旬被确认为全球威胁。为了尽量减少与他人的传播,最好的策略是将嫌疑人与其他人分开。
在非传播你的疾病大学(NSYSU),有许多学生团体。同一小组中的学生经常相互交流,一名学生可以加入几个小组。为了防止SARS的可能传播,NSYSU收集所有学生团体的成员名单,并在其标准操作程序(SOP)中制定以下规则。
一旦组中的成员是可疑成员,则该组中的所有成员都是可疑成员。
然而,他们发现,当一个学生被认定为嫌疑人时,要识别所有嫌疑人并不容易。你的工作是编写一个程序来找到所有的嫌疑人。

输入

输入文件包含几种情况。每个测试用例以一行中的两个整数 n 和 m 开头,其中 n 是学生数,m 是组数。您可以假设 0 < n <= 30000 和 0 <= m <= 500。每个学生都由 0 到 n−1 之间的唯一整数编号,最初学生 0 在所有情况下都被视为嫌疑人。此行后跟组的 m 个成员列表,每个组一行。每行本身都以整数 k 开头,表示组中的成员数。在成员数量之后,有 k 个整数表示此组中的学生。一行中的所有整数至少由一个空格分隔。
n = 0 且 m = 0 的情况表示输入结束,无需处理。

输出

对于每个案例,输出一行中的可疑数量。

示例输入

100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
示例输出

4
1
1                                                                                                                

思路:这个其实就是感染问题,只要统计多少人和 编号为 0 的人有接触就行,所以问题就转化为了,求 0 的根节点对应的那个集合有多个元素就行  ,可以定义一个num数组,初始化为1,在不断相加每个集合对应的个数就可以求出集合中元素的总的个数

统计对应元素集合中的元素个数:

void join(int x,int y)
{
    int fx=find(x),fy=find(y);
    if(fx!=fy)
    {
        pre[fx]=fy;
        num[fy]+=num[fx];
    }
}

完整代码: 

#include<cstdio>
#include<iostream>
using namespace std;
const int maxn=31000;
int pre[maxn],a[maxn],num[maxn];
int find(int x)
{
    int r=x;
    while(r!=pre[r])
        r=pre[r];
    int i=x,j;
    while(pre[i]!=r)
    {
        j=pre[i];
        pre[i]=r;
        i=j;
    }
    return r;
}
void join(int x,int y)
{
    int fx=find(x),fy=find(y);
    if(fx!=fy)
    {
        pre[fx]=fy;
        num[fy]+=num[fx];
    }
}
int main()
{
    int n,m,k;
    while(~scanf("%d%d",&n,&m),n+m)
    {
        for(int i=0; i<n; i++)
            pre[i]=i,num[i]=1;;
        while(m--)
        {
            scanf("%d",&k);
            for(int i=1; i<=k; i++)
                scanf("%d",&a[i]);
                for(int j=2; j<= k; j++)
                {
                    join(a[j-1],a[j]);
                }
        }
        printf("%d\n",num[find(0)]);
    }
 
    return 0;
}

 经典题:

食物链:(poj 1182)

动物王国中有三类动物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),输出假话的总数。

Input

第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。

Output

只有一个整数,表示假话的数目。

Sample Input

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

Sample Output

3

对于这题写的时候真的是一点思路都没有,难的一批,最后发现了一种方法发现只要想通了,这题还是挺简单的,这题对于 A,B,C 三个不同物种的动物关键是要表达出它们之间的关系,A->B->C->A,是一个环形的结构,这题直接套模板就不够用了,因为简单的并查集是不能表现出各个集合之间的关系,所以我们必须自己构造出一种方法来表达,那我们不妨可以这样,根据题意可以分为三类:

  • 和我同类
  • 被我吃的
  • 吃我的

我们可以用一个数组来表示(A,B,C中所有的动物都用这个方式来存储与别的动物的关系),如图:0-n表示同类,n-2n表示食物,2n-3n表示天敌

 根据题意可以得出,

当D等于1的时候我们需要判断x的根是不是和y的食物,天敌的跟相同,不相同的话就是同类,否则为假,为真的时候我们还要做一个操作,将它们的天敌和food合并起来,因为每个动物都有编号,这样以在下次判断x,y的时候可以清楚它的关系,则有

join(x,y);//同类
join(x+n,y+n);//相同食物
join(x+2*n,y+2*n);//相同天敌

D = 2 的时候我们只需判断x,y是不是同类,或者y是不是x的天敌(不需要判断x是不是y天敌,因为时x吃y),跟上面一样判断跟是否相同就行,之后我们也要做合并的操作(可以结合上面给出的图来理解)

join(x,y+2*n);//x吃y,那么y的天敌就和x是同类(环形)
 join(x+n,y);// x 的食物和y就是同类
 join(x+2*n,y+n); // x的天敌就是y的食物

知道后就可以AC了,注意的是应该用scanf读入,cin的话会超时 

#include<iostream>
#include<cstdio>
using namespace std;
#define MAX_N 50005
int fa[MAX_N*3];
int n,k , d, x, y;


void inint()
{
	for(int i=0;i<3*n;i++)
		fa[i] = i;
}


int find(int x )
{
	if(x == fa[x]) return x;
	else
	return fa[x] = find(fa[x]);
}

void join(int x , int y)
{
	x = find(x);
	y = find(y);
	if(x != y)
	fa[x] = y;
}


int main()
{
	int sum = 0;
	scanf("%d%d",&n,&k);
	inint();
	while(k--)
	{
		scanf("%d%d%d",&d,&x,&y);
		if(x > n || y > n ||(d == 2 && x == y))
		{
			sum++;
			continue;
		}
		else
		{
			if(d == 1)
			{
				if(find(x) == find(y+n) || find(x) == find(y+2*n))
				{
					sum++;
					continue;
				}
				else
				{
					join(x,y);
					join(x+n,y+n);
					join(x+2*n,y+2*n);
				}
			}
			else
			{
				if(find(x) == find(y) || find(x+2*n) == find(y))
				{
					sum++;
					continue;
				}
				else
				{
					join(x,y+2*n);
					join(x+n,y);
					join(x+2*n,y+n);
				}
			}
		}
	}
	cout << sum;
	return 0;
} 

贪心算法 

贪心算法的定义:
贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
解题的一般步骤是:
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的局部最优解合成原来问题的一个解。

这个有点向上面Kruskal算法,在起点和终点之间的路段不断选择代价最小的路段,在最后到达终点时的所花代价肯定最小,同理可得只要将问题拆分为多个子问题,子问题满足最优以此来达到全局最优(其实学算法也一样,只有每个基础都搞懂了,你就是算法大佬,我感觉就在于多练)

下面小编给大家举几个常见的贪心问题

区间问题

今年暑假不AC(hdu 2037)

/**HDUOJ 2037
 * Problem Description
    “今年暑假不AC?”
    “是的。”
    “那你干什么呢?”
    “看世界杯呀,笨蛋!”
    “@#$%^&*%...”
    确实如此,世界杯来了,球迷的节日也来了,估计很多ACMer也会抛开电脑,奔向电视了。
    作为球迷,一定想看尽量多的完整的比赛,当然,作为新时代的好青年,你一定还会看一些其它的节目,
    比如新闻联播(永远不要忘记关心国家大事)、非常6+7、超级女生,以及王小丫的《开心辞典》等等,
    假设你已经知道了所有你喜欢看的电视节目的转播时间表,你会合理安排吗?(目标是能看尽量多的完整节目)
 
    Input
    输入数据包含多个测试实例,每个测试实例的第一行只有一个整数n(n<=100),表示你喜欢看的节目的总数,
    然后是n行数据,每行包括两个数据Ti_s,Ti_e (1<=i<=n),分别表示第i个节目的开始和结束时间,为了简化问题,
    每个时间都用一个正整数表示。n=0表示输入结束,不做处理。
 
    Output
    对于每个测试实例,输出能完整看到的电视节目的个数,每个测试实例的输出占一行。
 
    Sample Input
    12
    1 3
    3 4
    0 7
    3 8
    15 19
    15 20
    10 15
    8 18
    6 12
    5 10
    4 14
    2 9
    0
    
 
    Sample Output
    5
 

思路: 对于这题由于由开始和结束的时间所以我们会想怎么根据这个时间来排,才能使其最多,由三种排的方法:(由下面的所给方法进行排序)

1.最早开始的时间

2.最早结束的时间

3.用时最少

分析:对(1)可以得到,如果最早开始的节目时长大于等于规定的时长,那么后面的节目就无法播放,所以第一个方法是错误的

对(3)可以得到,如果最后一个的节目时长最短,但是它的开始时间最晚,那么不可能前面的一段时间没有节目所以也是错的,

对(2)可得,(1)(3)错误那(2)不肯定对的呀,嘿嘿开个玩笑,小编这么严谨肯定会跟你们解释的,来上图(根据最早结束的时间已经排序(后端点),升序)用线段表示开始和结束的时间段

不难发现,1的结束时间最短先排1,然后舍去和1的节目时间冲突的节目那就是2了,在将3排入,4的节目时间有冲突,将4舍去,将5排入,结果就是,1,3,5,舍去2,4,最多就可以排3个。 

步骤:1 结束时间升序排序

           2 删除冲突的节目时间

           3 重复2步骤直到为空 

代码实现:

#include<iostream>
#include<algorithm>
#define maxn 110
using namespace std;
typedef struct{
	int start,end;
}pe;
pe act[maxn];
bool cmp(pe a,pe b)
{
	return a.end < b.end; // 升序 <  降序 > 
}
int main()
{
	int n;
	cin >> n;
	for(int i = 0 ; i < n ; i++)
	{
		cin >> act[i].start >> act[i].end; // cin与c语言中的scanf等价 
	}
	sort(act,act+n,cmp);
	int sum  = 0 , first_end = -1; // first用于第一个区间的比较 
	for(int i = 0 ; i < n ; i++ )// 要满足前一个节目的开始时间大于后一个节目的结束时间 
	{
		if(act[i].start >= first_end)
		{
			sum++;
			first_end = act[i].end;
		}
	}
	cout << sum ; // 等价于printf 
	return 0;
}

最少硬币问题 

最少硬币数量

某人带着3种面值的硬币去购物,有1元、2元、5元的,硬币的数量不限,他需要支付M元,问:怎么支付才能使硬币的数量最少

思路:根据生活常识应该是先使用面值最大的5元硬币来进行支付,第二次拿出第二大的硬币2元进行 支付,最后使用最小的,这样就能使的支付的硬币最少:

#include<iostream>
using namespace std;
const int num = 3;
const int value[num] = {1,2,5};
int main()
{
	int money;
	int ans[num] = {0}; // 用来记录每种硬币的数量
	cin >> money;
	for(int i = num -1 ; i >= 0 ; i--) // 数组下标从0开始 , 先使用最大面值
	{
		ans[i] = money / value[i];
		money -= ans[i]*value[i];
	} 
	for(int i = num -1 ; i >= 0 ; i--)
	{
		cout << value[i] << "元硬币的数量" << ans[i] << endl;
	}
	return 0;
} 

特例:不是所有的硬币问题都满足此种的贪心算法,例如:

面值 2 , 3 , 5 元的硬币支付 9元,用贪心算法(优先使用最大面值)是得不出答案的:9 - 5 = 4   ,  4 - 3 = 1 然而并没有1的面值硬币 ,但是实际上我们可以 5 + 2 + 2 进行支付,则得出方法错误,对于这样的任意的硬币问题小编接下来会在下面的动态规划算法中解释。常见的可以用贪心算法进行解题的硬币问题简单的判断标准是:面值是C的幂 ,C^0  C^1 C^2.......C^n,可以用贪心解答

后续算法容小编慢慢学一下,后续有好的题目小编会继续补充,也可以私信我哦,嘿嘿,记得点赞哦


动态规划

基本dp

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值