这是第十一周了,这周格外的忙碌,因为要竞赛、做题,同时也要结束并查集和拓扑排序的专题了,并且下周准备开始做下一个专题——最短路最小生成树的题目了。这周呢,自己主要是在做那50道题,同时最小生成树的知识也看了一些并且整理了一些例题,最后的一部分内容就是期中总结了。下面是详细的总结。
最小生成树、贪心
贪心算法
贪心算法并不从整体最优考虑,做出的选择只是局部最优解,即使不能得到整体最优解,最终结果也是最优解的很好近似。一般单源最短路径问题,最小生成树问题,可以产生整体最优解。基本的思路就是,将问题分解为若干个子问题,找出适合的贪心策略,然后求解每一个子问题的最优解,最后将局部最优解堆叠成全局最优解。需要注意的是,贪心算法不能保证求得的最后解是最佳的,也不能用来求最大或最小解问题。
376.摆动序列,看一下这个例题吧,题目要求从原始序列中删除一些元素来获得子序列,剩下的元素保持其原始顺序,求解最大摆动序列。
先从局部考虑,可以发现在单调坡上,删除一个坡度的结点,就可以得到两个局部峰值,然后再从整体考虑,只要使得整个序列出现最多的局部峰值,那么摆动序列就是最长的,通俗点就是,一个单调坡上不能出现第三个点,否则不是摆动序列,通过局部推整体,用贪心解决。基本的思路就是要删除单调坡上的结点,然后统计峰值个数就可以了。处理的过程当中,最左边和最右边的情况是不好处理的,如果只有两个数的话,可以变成三个数,让起始坡度为零,默认最右面有一个峰值,如果当前的差值大于零,前一个差值小于等于零,就让结果加1,处理过程还是很容易理解的。
给出核心处理代码:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int curDiff = 0;
int preDiff = 0;
int result = 1;
for (int i = 1; i < nums.size(); i++) {
curDiff = nums[i] - nums[i - 1];
if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
result++;
preDiff = curDiff;
}
}
}
最小生成树
实现最小生成树有两种算法,一个是prim算法,另一个是kruskal算法。prim算法的基本思路,就是在每次循环中都让一个新的点加入生成树,把所有的点包含进去,同时每次也都让一条边加进生成树,n-1次循环就能形成一棵树,因为每次都是取最小的边加入生成树,循环结束后得到的就是最小生成树。kruskal算法每次都选择一条最小的,且能合并两个不同集合的边,一共n个点,选取n-1条边,每次选的都是最小的边,最后形成的就是最小生成树。kruskal算法的基本思路,就是先将所有的边从小到大排序,按顺序枚举每一条边,如果这条边连接两个不同的集合,就把这条边加入最小生成树,这两个集合合并为一个集合,如果是同一个集合 就跳过,直到选取n-1条边。
这两类题主要是套用模板,整理了一个模板题便于以后套用,P3366 【模板】最小生成树两种方法都试了一下,prim算法超时了,kruskal算法可以通过,给出kruskal模板代码:
// Kruskal 算法求最小生成树
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
struct node {
int x, y, z;
}edge[maxn];
bool cmp(node a, node b) {
return a.z < b.z;
}
int fa[maxn];
int n, m;
int u, v, w;
long long sum;
int get(int x) {
return x == fa[x] ? x : fa[x] = get(fa[x]);
}
int main(void) {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin>>edge[i].x>>edge[i].y>>edge[i].z;
}
for (int i = 0; i <= n; i++) {
fa[i] = i;
}
sort(edge + 1, edge + 1 + m, cmp);
for (int i = 1; i <= m; i++) {
int x = get(edge[i].x);
int y = get(edge[i].y);
if (x == y) continue;
fa[y] = x;
sum += edge[i].z;
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (i == fa[i]) ans++;
}
if (ans > 1) {
cout << "orz" << endl;
}
else {
cout << sum << endl;
}
return 0;
}
小技巧
稀疏图一般选择prim算法,采用邻接矩阵进行存储边之间的关系。稠密图一般选择Kruskal算法,采用邻接表进行存储边之间的关系,套用模板解决就可以了,需要多做些题目来巩固加强。
洛谷题目总结
这周主要还是在做并查集和拓扑排序的题目,整理了几道比较典型的题目。
P1983 [NOIP2013 普及组] 车站分级,这是一道要用拓扑排序解决的问题,题意就是说每个火车站都有一个级别,如果火车停靠了火车站 x,则始发站、终点站之间所有级别大于等于火车站x的都必须停靠,现有m趟车次的运行情况,求这n个火车站至少分为几个不同的级别。基本思路,首先可以想到的是,假如火车停下了,说明这个火车站的级别一定比其它的没有停下来的火车站的级别高,从停下的站点向没有停留的站点建立一条有向边,将每一个列车都这样处理,然后进行拓扑排序,排序后的拓扑图有几层,说明级别就有多高。如果用火车站来考虑不好想的话,感觉转化成火车的等级也是可以的。
还有一类是种类并查集的问题,P1525 [NOIP2010 提高组] 关押罪犯,题意呢,就是说有n名罪犯,m对罪犯有矛盾,矛盾值为c,有两座监狱,应如何分配罪犯,才能使市长看到的冲突事件的影响力最小。这个题目比较好考虑的一点就是,因为市长只看第一个影响力大的那一个,所以呢,处理的时候首先要遵循的就是要把矛盾大的两个罪犯一定要分开,可以先排一下序,将存在矛盾且矛盾大的两个罪犯分到两个不同的监狱,每个监狱用一个并查集来表示。具体的实现思路,可以定义结构体存储罪犯间的关系,以矛盾值作为判断依据,如果查询到x和y在同一座监狱,说明无法再继续优化分配,之前矛盾大的也分配完成了,此时的冲突事件影响力最小。
给出大佬关键代码,还是包含初始化,查找,合并三个基本的操作:
void Init() {
for (int i = 1; i <= 2 * n; i++) {
f[i] = i;
h[i] = 0;
}
}
int Find(int i) {
return f[i] == i ? f[i] : f[i] = Find(f[i]);
}
void merge(int a, int b) {
int fa = Find(a);
int fb = Find(b);
if (fa != fb) {
if (h[fa] < h[fb]) {
f[fa] = fb;
}
else {
f[fb] = fa;
if (h[fa] == h[fb]) h[fa]++;
}
}
}
int main() {
scanf("%d %d", &n, &m);
Init();
for (int i = 1; i <= m; i++)
scanf("%d %d %d", &a[i].x, &a[i].y, &a[i].c);
sort(a + 1, a + 1 + m, cmp);
for (int i = 1; i <= m; i++) {
int x = a[i].x;
int y = a[i].y;
if (Find(x) == Find(y)) {
printf("%d\n", a[i].c);
return 0;
}
merge(x, y + n);
merge(y, x + n);
}
最后再来看一个P3243 [HNOI2015]菜肴制作的问题吧,这个题需要用到优先队列,存反图来解决,还是比较新颖的。题意很明确,就是给定了一个优先顺序的序列,越靠前质量越高,然后再给出一些限制条件,求最优的菜肴制作顺序。用拓扑排序判断,如果拓扑排序不成功,说明存在环,不能得出最优的制作顺序。对于要求编号小的靠前输出的这类题,一般就要考虑反向建图。如果正向拓扑排序的话不一定准确,因为并不是序号小的一定先做,所以要建反图,从后往前按序号从大到小排序,倒序输出答案,把序号大的尽量放在后面,每次队列都取最大值。注意这一点之后,按照一般思路就可以解决了。
期中学习总结
感觉到了现在,自己收获还是挺大的,从搜索开始,一直到现在,其中也学习了并查集,拓扑排序,贪心等等算法。一开始还什么都不熟悉,现在已经了解了这么多算法,并且也能够解决一部分题目了。可能还是不够熟练吧,以后需要做的就是做题巩固了,不断提高自己的做题水平,目前自己可以稍微容易点解决的大部分是模板比较明显的题目,可能题目稍微绕点弯我就不会做了,还是需要加强呀!
半个学期已经过去了,平常看博客做题写总结确实很有用处,能够收获很多知识,见识很多题型以及思路,有自己的理解和思考在里边。每次写完博客之后感觉对知识的理解更深了,也很有成就感,感觉一周的学习成果几乎都包含在了博客里边。但是,感觉在学习的过程当中,总是感觉自己忽略了很重要的一些东西,虽然平时看的博客和题目都挺多的,一般的思路方法在脑子里都有了一个印象,感觉自己掌握了很多,但是一到做题的时候,好像只有这些印象还远远不够,很多时候我知道这个题自己总结过思路,但是把思路真正的转化为代码,自己有的时候实现的很是艰难,可能平时对这方面有些欠缺吧,所以很多时候不能真正的将思路转化成代码,这一点需要不断加强。
学习算法是一件周期很长的事情,不可能短时间内就把所有知识都吃透,把题目都做熟。希望以后可以继续做相应的题目进行总结,一步步的、脚踏实地的提高自己的算法水平。无论结果如何,还是要不断坚持下去,继续努力前行吧!!!为下周新一轮专题做好准备!!!