第十一周ACM博客总结

这是第十一周了,这周格外的忙碌,因为要竞赛、做题,同时也要结束并查集和拓扑排序的专题了,并且下周准备开始做下一个专题——最短路最小生成树的题目了。这周呢,自己主要是在做那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]菜肴制作的问题吧,这个题需要用到优先队列,存反图来解决,还是比较新颖的。题意很明确,就是给定了一个优先顺序的序列,越靠前质量越高,然后再给出一些限制条件,求最优的菜肴制作顺序。用拓扑排序判断,如果拓扑排序不成功,说明存在环,不能得出最优的制作顺序。对于要求编号小的靠前输出的这类题,一般就要考虑反向建图。如果正向拓扑排序的话不一定准确,因为并不是序号小的一定先做,所以要建反图,从后往前按序号从大到小排序,倒序输出答案,把序号大的尽量放在后面,每次队列都取最大值。注意这一点之后,按照一般思路就可以解决了。

期中学习总结

感觉到了现在,自己收获还是挺大的,从搜索开始,一直到现在,其中也学习了并查集,拓扑排序,贪心等等算法。一开始还什么都不熟悉,现在已经了解了这么多算法,并且也能够解决一部分题目了。可能还是不够熟练吧,以后需要做的就是做题巩固了,不断提高自己的做题水平,目前自己可以稍微容易点解决的大部分是模板比较明显的题目,可能题目稍微绕点弯我就不会做了,还是需要加强呀!

半个学期已经过去了,平常看博客做题写总结确实很有用处,能够收获很多知识,见识很多题型以及思路,有自己的理解和思考在里边。每次写完博客之后感觉对知识的理解更深了,也很有成就感,感觉一周的学习成果几乎都包含在了博客里边。但是,感觉在学习的过程当中,总是感觉自己忽略了很重要的一些东西,虽然平时看的博客和题目都挺多的,一般的思路方法在脑子里都有了一个印象,感觉自己掌握了很多,但是一到做题的时候,好像只有这些印象还远远不够,很多时候我知道这个题自己总结过思路,但是把思路真正的转化为代码,自己有的时候实现的很是艰难,可能平时对这方面有些欠缺吧,所以很多时候不能真正的将思路转化成代码,这一点需要不断加强。

学习算法是一件周期很长的事情,不可能短时间内就把所有知识都吃透,把题目都做熟。希望以后可以继续做相应的题目进行总结,一步步的、脚踏实地的提高自己的算法水平。无论结果如何,还是要不断坚持下去,继续努力前行吧!!!为下周新一轮专题做好准备!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暗紫色的乔松(-_^)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值