这周继续肝
早睡早起和晨跑的习惯要坚持下来
因为某些事情打乱了生物钟没关系,之后马上调整过来就好
周一 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也就是中转点,每次加入一个新的中转点,就可以枚举所有的情况
其实挺暴力的,这样枚举那么任何一条最短路,中间的所有节点都可以被枚举到
可以写出方程
我们发现每次dp第一维k只和k-1有关
可以把第一维去掉
这样f[i][j], f[i][k], f[k][j]其实就是k-1时的信息
这时我在想,这样处理了f[i][j],那么和原来不同的是后来dp的时候前面处理了f[i][j]会影响后面的转移
而如果有第一维的话就不会影响后面的转移
我就看了一下,如果前面更新了f[i][j],后来又出现了f[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 (杂题)
最后一场选拔赛打完了,发挥的不是很好
接下来把能力范围里的题目都补完
这次比赛发挥不好的原因是,因为想快点做出来,所以一些题目没想清楚就按一个思路做了,结果思路是错的,浪费了很多时间
应该像平时做题那样静下心来想题,想清楚了才开始写题,不然就是浪费时间
其实静下心来想题目是效率最高效果最好的,我的心理素质还要加强
今天休息休息吧,这周训练挺多的了。下周就补题和欧拉回路