大一下第三周学习笔记

这周继续肝

早睡早起和晨跑的习惯要坚持下来

因为某些事情打乱了生物钟没关系,之后马上调整过来就好

 

周一 3.15(杂题 + AC自动机)

CF1383A String Transformation 1(建图 + 并查集 / 贪心)

这题想了很久,往贪心方面想,但是没想出正确的贪心,一直做不出来。

在有充分的思考下看了题解,感觉特别秀

这道题要解决两个问题,一是怎么处理相同的字符一起变换,而是怎么处理变化之后的字符可以和其他字符组成相同的字符

a[i]要变成b[i],那么就在点a[i] 到点b[i]连一条边。

元素代表点,而这条边代表转化,也就是一次操作

这种操作暗含了把所有相同的点合并在一起,直接看成了一个整体,也就解决了第一个问题

关于第二个问题,我们看看样例

当a与b相连,b与c相连,a与c就顺便相连了,就省了一次操作

这就可以想到并查集维护联通分量,这就解决了第二个问题

这真是太秀了,说实话是因为我做题太少,比较少做建图的问题,我以前做的很多图论题都是一看就是图论题,很少要考虑怎么建图

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int f[30], ans, n;

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

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);	
	
	int T; cin >> T;
	while(T--)
	{ 
		_for(i, 0, 25) f[i] = i;
		ans = 0;
		string a, b;
		cin >> n >> a >> b;

		int flag = 1;
		REP(i, 0, n)
		{
			if(a[i] > b[i])
			{
				flag = 0;
				break;
			}
			if(a[i] < b[i])
			{
				int ra = find(a[i] - 'a'), rb = find(b[i] - 'a');
				if(ra != rb)
				{
					ans++;
					f[ra] = rb;
				}
			}
		}
		if(!flag) cout << "-1" << endl;
		else cout << ans << endl;
	}
	
	return 0;
}

还有一种做法是贪心做法

我当时自己想的时候也是往贪心方面想,但是就是没有想到把所有相同字母归结在一起的操作

所以我在纠结以a排序还是以b排序,发现都不行

正确的做法时相同的归结在一起,f[i][j]是从i到j是否需要从i到j

其实这个时候只有0和1的区别,多了其实是一样的

然后就第一层i从小到大,第二层j从i+1到大循环

i从到大保证了前面转移的可以给后面的

j小到大保证了转移时不会超过b的大小

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

int f[30][30], n;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);	
	
	int T; cin >> T;
	while(T--)
	{ 
		memset(f, 0, sizeof(f));
		string a, b;
		cin >> n >> a >> b;

		int flag = 1;
		REP(i, 0, n)
		{
			if(a[i] > b[i])
			{
				flag = 0;
				break;
			}
			if(a[i] < b[i]) f[a[i] - 'a'][b[i] - 'a']++;
		}
		if(!flag) 
		{
			cout << "-1" << endl;
			continue;
		}
		
		int ans = 0;
		_for(i, 0, 19)
			_for(j, i + 1, 19)
				if(f[i][j])
				{
					ans++;
					_for(k, j + 1, 19) 
						f[j][k] += f[i][k];
					break;
				}
		cout << ans << endl;
	}
	
	return 0;
}

「一本通 2.4 练习 6」文本生成器(AC自动机 + dp)

充分思考后还是做不出就可以看题解,有时候是有些知识点你不知道

要把握好一个度,不要思考个十几分钟就看答案,起码半小时一小时,也不要一直一直死磕很久很久浪费时间

这道题就是AC自动机上跑dp,我以前从来没做过这样的题目,靠自己想是肯定想不出来的

dp[i][j]表示串长为i,以节点j为结尾时的方案数,转移父亲的方案数加到儿子就行了

在Trie树上跑,不要跑到字符串末尾就行了

其实蛮简单的,我就是不知道AC自动机上还可以跑dp

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 6000 + 10;
const int MOD = 10007;
int t[N][26], fail[N], dp[110][N], End[N], n, m, cnt = 1;

int add(int a, int b) { return (a + b) % MOD; }
int mul(int a, int b) { return 1ll * a * b % MOD; }

void add(string s)
{
	int len = s.size(), p = 1;
	REP(i, 0, len)
	{
		int id = s[i] - 'A';
		if(!t[p][id]) t[p][id] = ++cnt;
		p = t[p][id];
	}
	End[p] = 1;
}

void get_fail()
{
	_for(i, 0, 25) t[0][i] = 1;
	fail[1] = 0;
	queue<int> q;
	q.push(1);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		_for(i, 0, 25)
			if(t[u][i])
			{
				q.push(t[u][i]);
				fail[t[u][i]] = t[fail[u]][i];
				End[t[u][i]] |= End[t[fail[u]][i]];
			}
			else t[u][i] = t[fail[u]][i];
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	
	cin >> n >> m;
	_for(i, 1, n)
	{
		string s;
		cin >> s;
		add(s);
	}
	get_fail();
	
	dp[0][1] = 1;
	_for(i, 1, m)
		_for(p, 1, cnt)
			_for(k, 0, 25)
				if(!End[t[p][k]])
					dp[i][t[p][k]] = add(dp[i][t[p][k]], dp[i-1][p]);

	int ans = 0;
	_for(p, 1, cnt)
		ans = add(ans, dp[m][p]);
	int sum = 1;
	_for(i, 1, m)
		sum = mul(sum, 26);
	printf("%d\n", (sum - ans + MOD) % MOD);
	
	return 0;
}

到这AC自动机就暂时告一段落了,把提高篇上这个专题的题目都做完了,收获蛮大的

这本书真挺不错的,这个学期争取刷完这本书以及洛谷上的官方题单,时不时去cf做div1杂题

接下来我可以做一做最短路。最短路我很早就会了,但是我发现我一直停留在模板阶段,最短路的各种拓展,建图我都不会

接下来做一做

 

周二 3.16(Floyed)

Floyed算法

发现自己对最短路理解还很浅,这周搞搞最短路,做一下各种变形的题目

复习Floyed算法,同样深刻理解算法原理

Floyed的本质是动态规划

i到j的最短路,对于某个节点k,有两种情况,一是最短路经过了k,一种是不经过k

那么设f[k][i][j]为最短路经过1,2,3……k时i到j的最短路为多少

k也就是中转点,每次加入一个新的中转点,就可以枚举所有的情况

其实挺暴力的,这样枚举那么任何一条最短路,中间的所有节点都可以被枚举到

可以写出方程f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j] )

我们发现每次dp第一维k只和k-1有关

可以把第一维去掉f[i][j] = min(f[i][j], f[i][k] + f[k][j])

这样f[i][j], f[i][k], f[k][j]其实就是k-1时的信息

这时我在想,这样处理了f[i][j],那么和原来不同的是后来dp的时候前面处理了f[i][j]会影响后面的转移

而如果有第一维的话就不会影响后面的转移

我就看了一下,如果前面更新了f[i][j],后来又出现了f[i][j],这时恰好是f[k][i][j] = min(f[k-1][i][j], f[k-1][i][j])

对于这种情况去掉第一维也是没关系的

不一定只与上一行有关就可以去掉这一维,因为对后面的转移有影响,要看情况

#include<bits/stdc++.h> 
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 110;
int f[N][N], n, m;

int main()
{
	memset(f, 0x3f, sizeof(f)); //初始化为最大 
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		f[u][v] = f[v][u] = min(f[u][v], w); //注意重边		
	} 
	_for(i, 1, n) f[i][i] = 0; //自己与自己为0 
	
	_for(k, 1, n) //k为第一层循环,理解算法原理,不要写错。 
		_for(i, 1, n)
			_for(j, 1, n)
				f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
	
	return 0;
}

「一本通 3.2 例 1」Sightseeing Trip(Floyed求无向图最小环)

这个代码挺秀的

每次多了一个新点时,就判断能否构成一个更小的环

这时用原始的a数组

这个方案记录也非常地秀,还可以这么操作

#include<bits/stdc++.h> 
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 110;
int a[N][N], f[N][N], p[N][N], n, m;
vector<int> path;

void get_path(int x, int y)
{
	if(p[x][y] == 0) return;
	get_path(x, p[x][y]);
	path.push_back(p[x][y]);
	get_path(p[x][y], y);
}

int main()
{
	memset(a, 0x3f, sizeof(a));
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		a[u][v] = a[v][u] = min(a[u][v], w);
	}
	_for(i, 1, n) f[i][i] = 0;
	
	int ans = 1e9;
	memcpy(f, a, sizeof(a));
	_for(k, 1, n)
	{
		REP(i, 1, k)
			REP(j, i + 1, k)
				if((long long)a[i][k] + a[j][k] + f[i][j] < ans)
				{
					ans = a[i][k] + a[j][k] + f[i][j];
					path.clear();
					path.push_back(i);
					get_path(i, j);
					path.push_back(j);
					path.push_back(k);
				}
		
		_for(i, 1, n)
			_for(j, 1, n)
				if(f[i][j] > f[i][k] + f[k][j])
				{
					f[i][j] = f[i][k] + f[k][j];
					p[i][j] = k;
				}
	}
	
	if(ans == 1e9) puts("No solution.");
	else
	{
		REP(i, 0, path.size())
			printf("%d ", path[i]);
		puts("");
	}
	
	return 0;
}

Floyed还可以用来做有向图的连通性问题,并查集只能处理无向图的。有边的设为0,其他为正无穷,求最短路,为0就是联通的

 

Bellman-Ford 算法以及队列优化(SPFA)

dijkstra不能处理负权边,Bellman-Ford可以处理有负权边的情况,还可以判断是否有负环

所以如果题目存在负权边那就不能用dijkstra,要用Bellman-Ford

我发现Bellman-Ford算法也非常暴力

定义松弛操作,对于一条边,d[v] = min(d[v], d[u] + w)为一次松弛操作

那么我们把每一条边都做一次松弛操作

那么这时距离原点的最短路只有一条边的d值就确定下来了

我们再来每一条边松弛一遍,这时距离原点的最短路有两条条边的d值就确定下来了

一个最短路最长有n - 1条边

所以我们最多做n-1次,就可以求出最短路

时间复杂度为O(nm)

那么这个显然是可以优化的,因为存在很多无用的松弛

那么我们考虑什么样的松弛是可能起作用的

d[v] = min(d[v], d[u] + w) 看这个松弛式子

一开始d[u]是正无穷,只有当d[u]被更新了,才可能更新。

同样,后来也是只有d[u]变得更小了,才有可能跟新

所以结论是只有上一次d值改变的点所连接的边才有可能松弛成功

所以我们可以用队列优化,把每次松弛成功的点加入队列。一个点可能被加入队列多次。

如果已经再队列中就不用再加入了,用一个vis数组判断一下

这样队列优化就可以免去很多无用的松弛

那么这么判断负环呢?显然一条边最多松弛n-1次

那么如果一个点出队松弛次数大于等于n,那它的边也就是松弛次数大于等于n,那就存在负环

spfa复杂度为O(km),k为比较小的常数

但是可以构造数据卡回到O(nm)

所以一般最短路不写spfa,但是涉及到负环负权边就只能用spfa了

_for(i, 1, n) d[i] = 1e9;
d[s] = 0;
queue<int> q;
q.push(s);
vis[s] = 1;

while(!q.empty())
{
	int u = q.front(); q.pop();
	vis[u] = 0;
	if(++cnt[u] >= n) 
	{
		puts("负环"); 
		break;
	}
	for(auto x: g[u])	
	{
		int v = x.v, w = x.w;
		if(d[v] > d[u] + w)
		{
			d[v] = d[u] + w;
			if(!vis[v])
			{
				q.push(v);
				vis[v] = 1;
			}
		}
	}
} 

周三 3.17 (最短路)

孤岛营救问题(bfs求最短路 + 状态压缩)

这题的难点在于状态压缩,其实看到数据范围很小的时候就可以考虑状态压缩

状态压缩有几个注意点

1.初始化问题。最好写|= 而不是=,因为初始化的时候可能有多个信息,比如一个房间有多把钥匙

2.重复入队问题。要用一个vis数组记录位置和状态,防止重复入队

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 15;
int door[N][N][N][N], key[N][N], vis[N][N][1 << 10 | 1]; //最大1 << 10 
int n, m, p, k, s;
int dir[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
struct node{ int x, y, step, k; };

int main()
{
	scanf("%d%d%d%d", &n, &m, &p, &k);
	while(k--)
	{
		int x1, y1, x2, y2, g;
		scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &g);
		door[x1][y1][x2][y2] = door[x2][y2][x1][y1] |= 1 << g; //门需要钥匙的状态,初始0可以区分。|=适用于多扇门(虽然题目说只有一扇) 
	}
	scanf("%d", &s);
	while(s--)
	{
		int x, y, q;
		scanf("%d%d%d", &x, &y, &q);
		key[x][y] |= 1 << q; //巨坑,可能有多把钥匙,所以这种状态压缩的最好写|=而不是= 
	}
	
	queue<node> q;
	q.push(node{1, 1, 0, key[1][1]}); //注意一开始钥匙状态不是0 
	while(!q.empty())
	{
		node u = q.front(); q.pop();
		int x = u.x, y = u.y, k = u.k;
		if(x == n && y == m)
		{
			printf("%d\n", u.step);
			return 0;
		}
		REP(i, 0, 4)
		{
			int xx = x + dir[i][0], yy = y + dir[i][1];
			if(xx < 1 || xx > n || yy < 1 || yy > m) continue;
			if(door[x][y][xx][yy] && !(k & door[x][y][xx][yy])) continue; //k为有的钥匙,door为需要的钥匙,且成功代表可以开所有门 
			if(vis[xx][yy][k | key[xx][yy]]) continue; //点坐标和钥匙状态来确定vis,因为会重复入队。少了这一句会超时。 
			vis[xx][yy][k | key[xx][yy]] = 1;
			q.push(node{xx, yy, u.step + 1, k | key[xx][yy]});
		}
	}
	puts("-1");

	return 0;
}

「一本通 3.2 例 3」架设电话线(二分答案+建图+最短路)

这题好秀啊,真的秀

首先读完题就感觉是二分答案,但不是很确定

我不知道该这么判断这些边

中间我有最小生成树的想法,后来发现不行

因为路径的定义是s, u1, u2……un, t

除了s和t可以相同,其他的一定是不同的点

首先确定一个答案,就可以筛选出大于答案的这些边

我现在就要判断存不存在一条路径使得经过这些边的数目尽可能少,要小于等于k

所以我们把这些这些边长度设为1,其他边设为0,跑一遍最短路就可以判断出来

这个建图挺秀的。

其实我一开始看错题了,理解成第k+1小的

其实这个思路也可以做

把小于等于mid边设置为-1,其他设置为0

跑最短路,这时有负权边,要用spfa。

把有没有经过这些边抽象为边权为1,很秀

最短路的的难点其实在于建图

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1000 + 10;
const int M = 2000 + 10;
int g[N][N], u[M], v[M], w[M], vis[N], d[N];
int n, m, k; 
struct node{ int v, w; };

int spfa()
{
	_for(i, 2, n) d[i] = 1e9; d[1] = 0;
	queue<int> q;
	q.push(1);
	vis[1] = 1;
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		vis[u] = 0;
		_for(v, 1, n)
			if(g[u][v] != -1 && d[v] > d[u] + g[u][v])
			{
				d[v] = d[u] + g[u][v];
				if(!vis[v])
				{
					q.push(v);
					vis[v] = 1;
				}
			}
	}
	return d[n];
}

bool check(int key)
{
	memset(g, -1, sizeof(g));
	_for(i, 1, m)
	{
		if(w[i] > key) g[u[i]][v[i]] = g[v[i]][u[i]] = 1;
		else g[u[i]][v[i]] = g[v[i]][u[i]] = 0;
	}
	return spfa() <= k;
}
 
int main()
{
	scanf("%d%d%d", &n, &m, &k);
	_for(i, 1, m) scanf("%d%d%d", &u[i], &v[i], &w[i]);
	
	if(!check(1e9)) 
	{
		puts("-1");
		return 0;
	}
	
	int l = -1, r = 1e9; //l最好设置为-1,一定不可能这个答案,万一答案为0呢 
	while(l + 1 < r)    //题目没给边权范围,不管他直接开1e9 
	{
		int mid = (l + r) >> 1;
		if(check(mid)) r = mid;
		else l = mid; 
	}
	printf("%d\n", r);
 
	return 0;
}

「一本通 3.2 练习 1」农场派对(建反图)

之前在洛谷做过类似的题目,也是跑到地方然后回来

其实当时想了挺久才想到建反图,这次看到直接秒杀了

所以一个人的实力和他的做题量有很大关系,当然前提是做题大多是独立思考,而不是想一会想不出就看题解

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1000 + 10;
int g[2][N][N], d[2][N], n, m, s; 
struct node
{
	int v, w;
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};

void work(int k)
{
	_for(i, 1, n) d[k][i] = 1e9;
	d[k][s] = 0; 
	priority_queue<node> q;	
	q.push(node{s, 0});
	
	while(!q.empty())
	{
		node x = q.top(); q.pop();
		int u = x.v;
		if(d[k][u] != x.w) continue;
		_for(v, 1, n)
			if(d[k][v] > d[k][u] + g[k][u][v])
			{
				d[k][v] = d[k][u] + g[k][u][v];
				q.push(node{v, d[k][v]});
			}
	}
}

int main()
{
	memset(g, 0x3f, sizeof(g));
	scanf("%d%d%d", &n, &m, &s);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[0][u][v] = min(g[0][u][v], w);
		g[1][v][u] = min(g[1][v][u], w);
	}
	
	work(0);
	work(1);
	int ans = 0;
	_for(i, 1, n)
		ans = max(ans, d[0][i] + d[1][i]);
	printf("%d\n", ans);
 
	return 0;
}

周三 3.18(最短路)

[USACO06NOV]Roadblocks G(求次短路)

这道题我一开始的思路就是设两个d数组,一个次短路,一个最短路

那么我就想什么情况下会有次短路

d1最短,d2次短

d1[u] + w  和d2[u] + w都可以影响d2[v]

d1[v]照常就好

我写了交上去WA,过了一半的点

后来发现少考虑了一种情况,也就是当最短路更新时,值要赋给次短路

交上去WA一个点

最后发现初始化写错了,我初始化时d1d2都设为0

次短路应该长一点,我开始以为没什么影响,没想到就是错在这里

d2也设为最大

我一开始是习惯想dijsktra,然后觉得spfa这种加入队列松弛比较符合我的思路,就写了spfa

写完后发现洛谷题解第一篇和我思路一模一样

我觉得这道题难在考虑的点比较多吧,要考虑三种情况,要考虑全,还要注意初始化细节

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 5e3 + 10;
int d1[N], d2[N], vis[N], n, m;
struct node{ int v, w; };
vector<node> g[N];

void work()
{
	_for(i, 1, n) d1[i] = d2[i] = 1e9;
	d1[1] = 0;
	queue<int> q;
	q.push(1);
	vis[1] = 1;
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		vis[u] = 0;
		for(auto t: g[u])
		{
			int v = t.v, w = t.w;
			if(d1[v] > d1[u] + w)
			{
				d2[v] = d1[v];
				d1[v] = d1[u] + w;
				if(!vis[v]) { vis[v] = 1; q.push(v); }
			}
			if(d2[v] > d1[u] + w && d1[u] + w > d1[v]) 
			{
				d2[v] = d1[u] + w;
				if(!vis[v]) { vis[v] = 1; q.push(v); }
			}
			if(d2[v] > d2[u] + w && d2[u] + w > d1[v]) 
			{
				d2[v] = d2[u] + w;
				if(!vis[v]) { vis[v] = 1; q.push(v); }
			}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[u].push_back(node{v, w});
		g[v].push_back(node{u, w});
	}
	
	work();
	printf("%d\n", d2[n]);
	
	return 0;
}

后面我又尝试了用dijstra写

为什么我从dijsktra转到spfa呢

因为我一开始觉得,spfa可以重复入队,一个点可以加入多次松弛。

而对于dijsktra,一个点出队了意味着它就是已经确定了最短路了,那么这个点后来是不会再入队的(因为只有找到更短的路才会入队)。

这道题如果d2数组改变了话后面也是要再松弛的,也就是说出队后后来会入队

然后我发现,其实加入多次也没有关系,加就加呗,加进去让它以后再松弛一波,没有关系的

想后面再松弛就入队,和spfa类似

所以这是一个不那么标准的dijsktra

这两个算法又相似之处,都是把点存再队列里,如果更新了值就入队,以后让它去松弛

只不过dijskra出队那个就已经是最短路了。

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 5e3 + 10;
int d1[N], d2[N], n, m;
struct node{ int v, w; };
vector<node> g[N];

struct node2
{
	int v, w1, w2;
	bool operator < (const node2& rhs) const
	{
		return w1 > rhs.w1;
	}
};

void work()
{
	_for(i, 1, n) d1[i] = d2[i] = 1e9;
	d1[1] = 0;
	priority_queue<node2> q;
	q.push(node2{1, d1[1], d2[1]});
	
	while(!q.empty())
	{
		node2 x = q.top(); q.pop();
		int u = x.v;
		if(x.w1 != d1[u] || x.w2 != d2[u]) continue; //实现删除操作 
		for(auto t: g[u])
		{
			int v = t.v, w = t.w;
			if(d1[v] > d1[u] + w)
			{
				d2[v] = d1[v]; d1[v] = d1[u] + w;
				q.push(node2{v, d1[v], d2[v]}); //管它,扔到队列里,后面一定会松弛。 
			}
			if(d2[v] > d1[u] + w && d1[u] + w > d1[v]) 
			{
				d2[v] = d1[u] + w;
				q.push(node2{v, d1[v], d2[v]});
			}
			if(d2[v] > d2[u] + w && d2[u] + w > d1[v]) 
			{
				d2[v] = d2[u] + w;
				q.push(node2{v, d1[v], d2[v]});
			}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[u].push_back(node{v, w});
		g[v].push_back(node{u, w});
	}
	
	work();
	printf("%d\n", d2[n]);
	
	return 0;
}

周五 3.19 (最短路)

「一本通 3.2 练习 3」最短路计数

这题很容易想到用一个数组来保存次数

成功松弛的话cnt[v] = cnt[u]

相等的话cnt[v] += cnt[u]

但是我发现有一个问题

就是在执行cnt[v] += cnt[u]的时候

万一之后cnt[u]还没更新完怎么办

后来发现dijskra没这个问题

因为一个点只会出来松弛一次,而spfa一个点会松弛很多次

dijskra一个点出来松弛时它已经到了最短路。所以cnt[u]是不会再改变

而spfa cnt[u]可以再改变。所以洛谷题解里面有个说很多写spfa的去另外一道最短路计数的题过不了,这道题能过是因为数据水

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
const int MOD = 100003;
vector<int> g[N];
int d[N], cnt[N], n, m;

struct node
{
	int v, w;
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};

void work()
{
	_for(i, 2, n) d[i] = 1e9;
	cnt[1] = 1;
	priority_queue<node> q;
	q.push(node{1, d[1]});
	
	while(!q.empty())
	{
		node x = q.top(); q.pop();
		int u = x.v;
		if(d[u] != x.w) continue;
		for(auto v: g[u])
		{
			if(d[v] > d[u] + 1)
			{
				d[v] = d[u] + 1;
				cnt[v] = cnt[u];
				q.push(node{v, d[v]});	
			}	
			else if(d[v] == d[u] + 1) cnt[v] = (cnt[v] + cnt[u]) % MOD;
		} 
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	
	work();
	_for(i, 1, n) printf("%d\n", cnt[i]); 
	
	return 0;
}

写完看了洛谷题解发现还可以用bfs做

bfs,第一次搜到的就是最短路,搜过的点不需要再搜,所以是O(n)的

这时在bfs搜索树中,深度少1的可以更新该深度的次数

所以bfs记录深度就好了

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
const int MOD = 100003;
vector<int> g[N];
int dep[N], cnt[N], n, m;

void work()
{
	queue<int> q;
	dep[1] = 1; cnt[1] = 1;
	q.push(1);
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto v: g[u])
		{
			if(dep[v] == dep[u] + 1) cnt[v] = (cnt[v] + cnt[u]) % MOD;
			if(dep[v]) continue;
			dep[v] = dep[u] + 1;
			cnt[v] = cnt[u];
			q.push(v);
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	
	work();
	_for(i, 1, n) printf("%d\n", cnt[i]); 
	
	return 0;
}

「一本通 3.2 练习 4」新年好(最短路+全排列)

一开始没什么思路

但是洗澡时突然意识到,因为点很少,所以可以每个都求最短路,然后用排列枚举出每个去的顺序即可,完事

用到了next_permutation(a, a + n)不存在下一个排列返回0,存在返回1

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 5e4 + 10;
struct node
{
	int v, w;
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};
vector<node> g[N];
int d[10][N], a[10], n, m;

void work(int id, int p)
{
	_for(i, 1, n) d[id][i] = 1e9;
	d[id][p] = 0;
	priority_queue<node> q;
	q.push(node{p, d[id][p]});
	
	while(!q.empty())
	{
		node x = q.top(); q.pop();
		int u = x.v;
		if(d[id][u] != x.w) continue;
		for(auto t: g[u])
		{
			int v = t.v, w = t.w;
			if(d[id][v] > d[id][u] + w)
			{
				d[id][v] = d[id][u] + w;
				q.push(node{v, d[id][v]});
			}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	_for(i, 1, 5) scanf("%d", &a[i]);
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[u].push_back(node{v, w});
		g[v].push_back(node{u, w});
	}
	
	a[0] = 1;
	_for(i, 0, 5) work(i, a[i]);
	
	int ans = 1e9;
	int k[] = {1, 2, 3, 4, 5};
	while(1)
	{
		int sum = d[0][a[k[0]]];
		REP(i, 0, 4)
			sum += d[k[i]][a[k[i+1]]];
		ans = min(ans, sum);
		if(!next_permutation(k, k + 5)) break;
	}
	printf("%d\n", ans);
	
	return 0;
}

两个规划

一个是每周如果无比赛,自己就可以去牛客竞赛上打一场提高组比赛

发现这个平台有很多比赛

一个是训练不一定要在电脑前,今天一道题就是早上看了,然后下午突然就想到了

所以可以放一道题在脑子里,潜意识在思考,说不定什么时候就会了呢

 

周六 3.20 (最短路 + 拓扑排序)

[NOIP2009 提高组] 最优贸易(bfs+建反图)

想了一个小时,突然意识到一定是先买后卖,所以一条路径中买的点一定在卖的点的前面

这个非常重要,很关键

因此我们可以bfs求出从1开始到每个点买的最少价钱

然后bfs求出每个点到n卖的最多价钱(建反图)

然后枚举点就行了

独立做出,开心

做完看到洛谷题解里各种神仙做法,我这个做法算是比较直接简单的了

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10;
int d1[N], d2[N], a[N], s, n, m;
vector<int> g[2][N];

void work1()
{
	_for(i, 1, n) d1[i] = 1e9;
	queue<int> q;
	q.push(1);
	d1[1] = a[1];
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto v: g[0][u])
			if(min(a[v], d1[u]) < d1[v])
			{
				d1[v] = min(a[v], d1[u]);
				q.push(v);
			}
	}
}

void work2()
{
	_for(i, 1, n) d2[i] = 0;
	queue<int> q;
	q.push(n);
	d2[n] = a[n];
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto v: g[1][u])
			if(max(a[v], d2[u]) > d2[v])
			{
				d2[v] = max(a[v], d2[u]);
				q.push(v);
			}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	_for(i, 1, n) scanf("%d", &a[i]);
	while(m--)
	{
		int u, v, op;
		scanf("%d%d%d", &u, &v, &op);
		if(op == 1)
		{
			g[0][u].push_back(v);
			g[1][v].push_back(u);
		}
		else
		{
			g[0][u].push_back(v);
			g[0][v].push_back(u);
			g[1][u].push_back(v);
			g[1][v].push_back(u);
		}
	}

	work1(); work2();
	int ans = 0;
	_for(i, 1, n)
		ans = max(ans, d2[i] - d1[i]);
	printf("%d\n", ans);
	
	return 0;
}

「一本通 3.2 练习 7」道路和航线(spfa求负权图最短路+SLF优化)

这道题就是负权图,无负环求最短路

只能spfa,我写了之后发现T了两个点

然后去查发现有SLF优化,用到了双端队列

栈是一端进,一端出,队列是队尾进,队首出

而双端队列deque两端都可以进出

SLF优化就是使队列更接近优先队列,当前的d值小于队首则加入队首,否则加入队尾

优化还是挺明显了,从T到0.2s 

有个细节要注意,取队首的前提是队列非空

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 2.5e4 + 10;
int d[N], vis[N], n, m1, m2, s;
struct node{ int v, w; };
vector<node> g[N];

void spfa()
{
	_for(i, 1, n) d[i] = 1e9;
	d[s] = 0;
	deque<int> q;
	q.push_back(s);
	vis[s] = 1;
	
	while(!q.empty())
	{
		int u = q.front(); q.pop_front();
		vis[u] = 0;
		for(auto x: g[u])
		{
			int v = x.v, w = x.w;
			if(d[v] > d[u] + w)
			{
				d[v] = d[u] + w;
				if(!vis[v])
				{
					vis[v] = 1;
					if(!q.empty() && d[v] < d[q.front()]) q.push_front(v); //注意这里空队列时没有q.front() 
					else q.push_back(v);
				}
			}
		} 
	}
}

int main()
{
	scanf("%d%d%d%d", &n, &m1, &m2, &s);
	while(m1--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[u].push_back(node{v, w});
		g[v].push_back(node{u, w});
	}
	while(m2--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		g[u].push_back(node{v, w});
	}
	
	spfa();
	_for(i, 1, n)
	{
		if(d[i] == 1e9) puts("NO PATH");
		else printf("%d\n", d[i]);
	}
	
	return 0;
}

「网络流 24 题」汽车加油行驶问题(分层图建图+最短路)

一开始想暴力bfs,发现一直会往回走,不知道怎么去除无效的状态

然后我看到k<=10的时候就马上意识到了分层图

我知道这个思想,做题遇到是第一次

把这个二维的图,加个油的坐标,变成三维了

然后根据题意连边就行了

实现上给每个点赋予id,然后建图,然后跑最短路就好。

独立做出紫题,爽。

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 110;
const int M = N * N * 11;
int G[N][N], d[M];
int n, k, a, b, c, ans;
int dir[4][2] = {0, 1, 0, -1, 1, 0, -1, 0};
struct node{ int v, w; };
vector<node> g[M];

int id(int i, int j, int k) { return k * n * n + (i - 1) * n + j; } //设点坐标 

void build() //核心建图 
{
	_for(o, 0, k)
		_for(x, 1, n)
			_for(y, 1, n)
			{
				if(o != k && G[x][y]) //遇到油站 
				{
					g[id(x, y, o)].push_back(node{id(x, y, k), a});
					continue;
				}
				if(o != k) g[id(x, y, o)].push_back(node{id(x, y, k), a + c});  //设立油库且加油 
				
				REP(i, 0, 4)
				{
					int xx = x + dir[i][0], yy = y + dir[i][1], w = 0;
					if(xx < 1 || xx > n || yy < 1 || yy > n) continue;
					if(xx < x || yy < y) w += b;
					if(o) g[id(x, y, o)].push_back(node{id(xx, yy, o - 1), w}); //走一步 
				}
			}
}

void spfa()
{
	_for(i, 1, id(n, n, k)) d[i] = 1e9;
	d[id(1, 1, k)] = 0;
	queue<int> q; 
	q.push(id(1, 1, k));
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		for(auto t: g[u])
		{
			int v = t.v, w = t.w;
			if(d[v] > d[u] + w)
			{
				d[v] = d[u] + w; 
				q.push(v); 
			} 
		}
	}
}

int main()
{
	scanf("%d%d%d%d%d", &n, &k, &a, &b, &c);
	_for(i, 1, n)
		_for(j, 1, n)	
			scanf("%d", &G[i][j]);
	
	build();
	spfa();
	
	int ans = 1e9;
	_for(o, 0, k)
		ans = min(ans, d[id(n, n, o)]);
	printf("%d\n", ans);
	
	return 0;
}

到这书上最短路的题都全部做完了,爽

接下来补一下拓扑排序和欧拉回路

 

拓扑排序模板

发现拓扑排序挺简单的

在有向无环图中,求出点的排序,使得对于任意从u可以走到v

v都在u的后面

方法很简单,bfs维护一个入度为0的点的集合

一取出来就把它连的边删掉,入度变0就加入队列

拓扑排序可以来判断有无环,如果有环则拓扑排序结果会小于n

其实dfs也可以判断有无环,用vis数组,0表示没访问过,-1表示还在这次的dfs中,1表示之前的dfs访问过了。

拓扑排序还可以来判断是否是一条链

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e5 + 10; 
vector<int> g[N], topo;
int in[N], n, m;

void topo_sort()
{
	queue<int> q;
	_for(i, 1, n)
		if(!in[i])
			q.push(i);
	
	while(!q.empty())
	{
		int u = q.front(); q.pop();
		topo.push_back(u);
		for(auto v: g[u])
		{
			in[v]--;
			if(!in[v]) q.push(v);
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	while(m--)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		in[v]++;
	}
	
	topo_sort();
	if(topo.size() != n) puts("-1");
	else for(auto i: topo) printf("%d ", i);
	puts("");
	
	return 0;
}

[JLOI2011]飞行路线(分层图最短路)

又练习了一道分层图最短路

挺裸的一道题,看到题目一些特殊状态的值很小,比如这题小于等于10,那十有八九就是分层图最短路了

这次写了dijsktra,和spfa写起来差不多,这个还更优。以后就写dijsktra了

然后注意标号不太一样,题目从0开始,要注意处理一下,包括题目给的起点终点

读题的时候就要意识到

#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++) 
#define _for(i, a, b) for(int i = (a); i <= (b); i++) 
using namespace std;

const int N = 1e4 + 10;
const int M = N * 11;
int d[M], n, m, k, S, T;
struct node
{ 
	int v, w; 
	bool operator < (const node& rhs) const
	{
		return w > rhs.w;
	}
};
vector<node> g[N], G[M];

int id(int u, int t) { return t * n + u; } //在点u,已经用了t条免费航线 

void build()
{
	_for(t, 0, k)
		_for(u, 1, n)
			for(auto x: g[u])
			{
				int v = x.v, w = x.w;
				G[id(u, t)].push_back(node{id(v, t), w});
				if(t < k) G[id(u, t)].push_back(node{id(v, t + 1), 0});
			}
}

void work()
{
	_for(i, 1, id(n, k)) d[i] = 1e9;
	d[id(S, 0)] = 0;
	priority_queue<node> q;
	q.push(node{id(S, 0), 0});
	
	while(!q.empty())
	{
		node x = q.top(); q.pop();
		int u = x.v;
		if(d[u] != x.w) continue;
		for(auto t: G[u])
		{
			int v = t.v, w = t.w;
			if(d[v] > d[u] + w)
			{
				d[v] = d[u] + w;
				q.push(node{v, d[v]});
			}
		}
	}
}

int main()
{
	scanf("%d%d%d%d%d", &n, &m, &k, &S, &T);
	S++; T++;
	while(m--)
	{
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		u++; v++;
		g[u].push_back(node{v, w});
		g[v].push_back(node{u, w});
	}
	
	build();
	work();
	
	int ans = 1e9;
	_for(t, 0, k)
		ans = min(ans, d[id(T, t)]);
	printf("%d\n", ans);
	
	return 0;
}

周日 3.21 (杂题)

最后一场选拔赛打完了,发挥的不是很好

接下来把能力范围里的题目都补完

这次比赛发挥不好的原因是,因为想快点做出来,所以一些题目没想清楚就按一个思路做了,结果思路是错的,浪费了很多时间

应该像平时做题那样静下心来想题,想清楚了才开始写题,不然就是浪费时间

其实静下心来想题目是效率最高效果最好的,我的心理素质还要加强

今天休息休息吧,这周训练挺多的了。下周就补题和欧拉回路

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值