ACM-ICPC模板
一、线段树优化的$Dijkstra$
优先队列版本已经烂大街了,这里就不贴了,而且在下面的分层图里有写
1.普通线段树
时间和内存均是优先队列优化版本的 $\frac{1}{2}$
int n, m;
struct edge {
int to, w, nxt;
edge() {}
edge(int t, int ww, int nn) {to = t, w = ww, nxt = nn;}
}e[maxn << 1];
int head[maxn], k = 0;
void add(int u, int v, int w) {e[k] = edge(v, w, head[u]); head[u] = k++;}
ll ans[maxn];
struct node {
ll dis; int x;
node() {}
node(ll d, int xx) {dis = d, x = xx;}
}dis[maxn << 2];
//建树初始化,主要是编号也要返回所以要先预处理一下
void build(int p, int l, int r) {
if(l == r) {dis[p].x = l; return;}
int mid = l + r >> 1;
build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
dis[p].x = dis[p << 1].x;
}
void change(int p, int l, int r, int x, int y) {
if(l == r) {dis[p].dis = y; return;}
int mid = l + r >> 1;
if(x <= mid) change(p << 1, l, mid, x, y);
else change(p << 1 | 1, mid + 1, r, x, y);//单点修改的板子操作
if(dis[p << 1].dis < dis[p << 1 | 1].dis) dis[p] = dis[p << 1];
else dis[p] = dis[p << 1 | 1];
}
//因为用距离得到最小,但是需要的是编号,所以返回node
node ask(int p, int l, int r, int ls, int rs) {
if(ls <= l && r <= rs) {return dis[p];}
int mid = l + r >> 1; node ans = node(inf, 0), tmp;
if(ls <= mid) ans = ask(p << 1, l, mid, ls, rs);
if(rs > mid) {
node tmp = ask(p << 1 | 1, mid + 1, r, ls, rs);
if(ans.dis > tmp.dis) ans = tmp;
}
return ans;
}
int S;
void dij() {
for(int k = 1; k < n; k++) {//n-1次够用的。虽然我也不知道为什么最后n次跑的比n-1次还要快……
register int u = ask(1, 1, n, 1, n).x;
for(int i = head[u]; ~i; i = e[i].nxt) {
register int v = e[i].to;
if(ans[u] + e[i].w < ans[v]) {//最短路更新
ans[v] = ans[u] + e[i].w, change(1, 1, n, v, ans[v]);//单点修改
}
}
change(1, 1, n, u, inf);//取出来过后要赋值INF,以免再次取用
}
}
int main() {
memset(head, -1, sizeof head);
n = read(), m = read(), S = read();
for(int u, v, w, i = 1; i <= m; i++) u = read(), v = read(), w = read(), add(u, v, w);
//初始化
for(int i = 1; i <= (n << 2); i++) dis[i].dis = inf;
for(int i = 1; i <= n; i++) ans[i] = inf;
//线段树初始化,dis是线段树,ans是答案
build(1, 1, n);
change(1, 1, n, S, 0); ans[S] = 0;
dij();
for(int i = 1; i <= n; i++) printf("%lld ", ans[i]);
return 0;
}
2.最强的zkw线段树优化版本
时间和内存均是优先队列优化版本的 $\frac{1}{4}$ %%%
#define rep(I, A, B) for (int I = (A); I <= (B); ++I)
#define dwn(I, A, B) for (int I = (A); I >= (B); --I)
#define erp(I, X) for (int I = head[X]; I; I = next[I])
const int maxn = 1e5 + 207, maxm = 2e5 + 207, inf = INT_MAX;
int v[maxm], w[maxm], head[maxn], next[maxm], tot;
int dist[maxn], mp[maxn << 2], M = 1;
int n, m, s;
template inline void read(T& t) {
int f = 0, c = getchar(); t = 0;
while (!isdigit(c)) f |= c == '-', c = getchar();
while (isdigit(c)) t = t * 10 + c - 48, c = getchar();
if (f) t = -t;
}
template void write(T x) {
if (x < 0) x = -x, putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + 48);
}
inline void ae(int x, int y, int z) { v[++tot] = y; w[tot] = z; next[tot] = head[x]; head[x] = tot; }
inline int cmp(int a, int b) { return dist[a] < dist[b] ? a : b; }
inline void build(int n) {
while (M < n + 2) M <<= 1;
mp[0] = n + 1;
}
inline void modify(int x, int nv) {
for (int i = x + M; dist[mp[i]] > nv; i >>= 1)
mp[i] = x;
dist[x] = nv;
}
inline void del(int x) {
for (mp[x += M] = 0, x >>= 1; x; x >>= 1)
mp[x] = cmp(mp[x << 1], mp[x << 1 | 1]);
}
inline void dijkstra(int s) {
rep(i, 0, n) dist[i] = inf;
build(n); modify(s, 0);
rep(t, 1, n - 1) {
int x = mp[1]; del(x);
erp(i, x) if (dist[v[i]] > dist[x] + w[i])
modify(v[i], dist[x] + w[i]);
}
}
int main() {
read(n),read(m),read(s);
rep(i, 1, m) {
int x, y, z; read(x),read(y),read(z); ae(x, y, z);
}
dijkstra(s);
rep(i, 1, n) write(dist[i]), putchar(' ');
puts("");
return 0;
}
3.路径还原
白书上的代码,没有优化是$O(n^2)$,正序输出最短路径
/*如果需要输出路径
可以用一个prev[ j ]来记录最短路上顶点 j 的前驱,
那么在o(|V|)的时间内完成最短路径的恢复。
在d[ j ]被d[ j ] = d[ k ] + cost[ k ][ j ]更新时,
修改prev[ j ] = k,这样就可以求出来prev的数组,
可以在以上算法中用路径前驱标记法来还原出来路径。*/
int cost[MAX_V][MAX_V]; //cost[u][v]表示边e=(u, v)的权值(不存在这条边时设为INF)
int d[MAX_V]; //从顶点s出发的最短距离
bool used[MAX_V]; //已经使用过的图中的点
int V; //顶点数
int prev[MAX_V]; //记录前驱点
//从s出发到各个顶点的距离
void dijkstra(int s)
{
fill(d, d + V, INF); //初始化
fill(used, used + V, false); //初始化
fill(prev, prev + V, -1);
d[s] = 0;
while(true)
{
int v = -1;
for(int u = 0; u < V; u++){
if(!used[u] && (v == -1 || d[u] < d[v])) v = u;
// 从尚未使用过的顶点中选择一个距离最小的顶点
}
if(v == -1) break; //没有可跟新的了,结束
used[v] = true;
for(int u = 0; u < V; u++){
d[u] = min(d[u], d[v] + cost[u][v]); //因为加入了一个点V所以所有的d都要再更新一遍
prev[u] = v;
}
}
}
vector get_path(int t) //到顶点t的最短路
{
vector path;
for(; t != -1; t = prev[t])
path.push_back(t);
reverse(path.begin(), path.end());
return path;
}
二、可以处理负权值的$SPFA$
SPFA要谨慎使用!
int nex[M],ver[M],head[N],edge[M],tot;
void add(int u,int v,int val){
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
queueq;
bool vis[N];
int d[N];
int n,m;
void spfa(int s){
//memset(d,0x3f,sizeof d);
for(int i = 1; i<= n;++i)
d[i] = 2147483647;
memset(vis,0,sizeof vis);
d[s] = 0;
vis[s] = 1;
q.push(s);
while(q.size()){
int x = q.front();
q.pop();
vis[x] = 0;
//扫描所有出边
for(int i = head[x];i;i = nex[i]){
int y = ver[i];
int z = edge[i];
if(d[y] > d[x] + z){
d[y] = d[x] + z;
if(!vis[y])//不在堆里就入堆并标记一下
q.push(y),vis[y] = 1;
}
}
}
}
int s;
int main()
{
cin>>n>>m>>s;
over(i,1,m){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
spfa(s);
over(i,1,n){
printf("%d ",d[i]);
}
return 0;
}
三、两种分层图最短路
分层次
分成$k+1$层图
const int INF = 0x3f3f3f3f;
const int N = 6e5+7;
int nex[N],ver[N],head[N],edge[N],tot;
int n,m,k,s,t;
int dist[N];
bool vis[N];
void init(){
memset(head,-1,sizeof head);
tot = 0;
memset(dist,0x3f,sizeof dist);
memset(vis,0,sizeof vis);
}
void add(int u,int v,int val){
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
void dijkstra(){
priority_queue,greater >q;
dist[1] = 0;
//vis[s] = 1;dijkstra和spfa不一样!这里不用标记,不然就走不动啦
q.push({dist[1],1});
while(q.size()){
PII now = q.top();//这种声明的变量后面不要用逗号运算符!!!
q.pop();
int u = now.second;
if(vis[u])continue;
vis[u] = 1;
for(int i = head[u]; i != -1 ;i = nex[i]){
int v = ver[i],val = edge[i];
if(!vis[v] && dist[v] > dist[u] + val){
dist[v] = dist[u] + val;
q.push({dist[v],v});
}
}
}
}
int main()
{
init();
scanf("%d%d%d",&n,&m,&k);
over(i,1,m){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
for(int j = 0;j <= k;++j){
add(x + j * n,y + j * n,z);//这一层和这一层连起来
add(y + j * n,x + j * n,z);
if(j != k){//只要还有就这一层和下一层连起来,并且权值为0
add(x + j * n,y + (j + 1) * n,0);
add(y + j * n,x + (j + 1) * n,0);
}
}
}
dijkstra();
int ans = INF;//dijkstra只是更新了一个dis数组,答案需要我们自己找
over(i,0,k)
ans = min(ans,dist[n + i * n]);
printf("%d\n",ans);
return 0;
}
分维度
这里是因为题目要求的答案是第k+1的权值最小,所以只求单条路径的权值即可,但是这样的更新并不能满足dijkstra的贪心性质,所以我们要选择SPFA算法。
/*POJ 3662 Telephone Lines*/
const int N = 3e3;
const int M = 5e4+7;//注意这里数据范围要用M
int d[N][N];
int n,m,k;
int nex[M],ver[M],head[N],edge[M],tot;
void add(int u,int v,int val){
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
queueq;
bool vis[N];
void spfa(int s){
memset(d,0x3f,sizeof d);
memset(vis,0,sizeof vis);
d[s][0] = 0;
vis[s] = 0;
q.push(s);
while(q.size()){
int x = q.front();
q.pop();
vis[x] = 0;
for(int i = head[x];i;i = nex[i]){
int y = ver[i],z = edge[i];
int w = max(d[x][0],z);//不同于往常的是每一条路径的 最大边 是这条路径的花费
if(d[y][0]>w){//先算普通的最短路
d[y][0] = w;
if(!vis[y])
q.push(y),vis[y] = 1;
}
//反正就是动规,暴力所有状态转移取最小,状态转移的方式就是可以免费。
//每次对每种路径都取一次最优
for(int p = 1;p <= k;++p){//然后再看免费的,不一定是几条边最优//不管怎么说肯定是从前往后地推出的
int w = min(d[x][p-1],max(d[x][p],z));//使最大边最小//主要这里的答案都是一条边的权值
//当前的状态是从x转移到y,其中这条边免费或者不免费
if(d[y][p] > w){
d[y][p] = w;
if(!vis[y])
q.push(y),vis[y] = 1;
}
}
}
}
}
int main(){
cin>>n>>m>>k;
over(i,1,m){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
spfa(1);
int ans = 1e9+7;
over(i,0,k){//注意是从0开始,因为也可以不选
//就真答案并不在掌握之中
//其实就是分层图,应该是 k + 1 层
ans = min(ans,d[n][i]);
}
if(ans == 1e9+7)
puts("-1");
else printf("%d\n",ans);
return 0;
}
四、Floyd算法,传递闭包
Floyd算法
设$D[k,i,j]$表示“经过若干个编号不超过k的结点”从i到j的最短路长度。
Floyd算法的实质是动态规划。
k是阶段,所以必须放在最外层循环。
i和j是附加状态,应该放置于内层循环。
int dis[1000][1000],n,m;
int main()
{
cin>>n>>m;
memset(dis,0x3f,sizeof dis);
for(int i = 1;i <= n;++i)
dis[i][i] = 0;
for(int i = 1;i <= m;++i){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
dis[x][y] = min(dis[x][y],z);//可能会重复
}
for(int k = 1;k <= n;++k)
for(int i = 1;i <= n;++i)
for(int j = 1;j <= n;++j)
dis[i][j] = min(dis[i][j],dis[i][k] + dis[k][j]);
for(int i = 1;i <= n;++i)
for(int j = 1;j <= n;++j)
printf("%d ",dis[i][j]);
return 0;
}
使用Floyd实现的传递闭包
int dis[1000][1000],n,m;
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;++i)
dis[i][i] = 1;
for(int i = 1;i <= m;++i){
int x,y;
scanf("%d%d",&x,&y);
dis[x][y] = dis[y][x] = 1;
}
for(int k = 1;k <= n;++k)
for(int i = 1;i <= n;++i)
for(int j = 1;j <= n;++j)
dis[i][j] |= dis[i][k] & dis[k][j];
//把整个关系梳理出来
}