[POI2007] ATR-Tourist Attractions
[POI2007] ATR-Tourist Attractions
题目背景
题目描述
给出一张有 \(n\) 个点 \(m\) 条边的无向图,每条边有边权。
你需要找一条从 \(1\) 到 \(n\) 的最短路径,并且这条路径在满足给出的 \(g\) 个限制的情况下可以在所有编号从 \(2\) 到 \(k+1\) 的点上停留过。
每个限制条件形如 \(r_i, s_i\),表示停留在 \(s_i\) 之前必须先在 \(r_i\) 停留过。
注意,这里的停留不是指经过。
输入格式
第一行三个整数 \(n,m,k\)。
之后 \(m\) 行,每行三个整数 \(p_i, q_i, l_i\),表示在 \(p_i\) 和 \(q_i\) 之间有一条权为 \(l_i\) 的边。
之后一行一个整数 \(g\)。
之后 \(g\) 行,每行两个整数 \(r_i, s_i\),表示一个限制条件。
输出格式
输出一行一个整数,表示最短路径的长度。
样例 #1
样例输入 #1
8 15 4
1 2 3
1 3 4
1 4 4
1 6 2
1 7 3
2 3 6
2 4 2
2 5 2
3 4 3
3 6 3
3 8 6
4 5 2
4 8 6
5 7 4
5 8 6
3
2 3
3 4
3 5
样例输出 #1
19
提示
对于 \(100\%\) 的数据, 满足:
- \(2\le n\le2\times10^4\)
- \(1\le m\le2\times10^5\)
- \(0\le k\le\min(20, n-2)\)
- \(1\le p_i<q_i\le n\)
- \(1\le l_i\le 10^3\)
- \(r_i, s_i \in [2,k+1], r_i\not=s_i\)
- 保证不存在重边且一定有解。
解析
一道看起来是道最短路其实就是最短路的状压DP,\(k\) 的范围决定了状压的内容就是必须停留的城市,
首先处理 \(1--k+1\) 每一座城市到 \(n\) 的最短路, 方便之后状态转移,
然后处理约束条件, 既然最多只有 \(20\) 座城市, 我们就可以先记录在经过城市 \(k\) 之前必须停留的城市,
之后在遍历状态时这个就是转移的条件. \(dp[i][j]\) 表示状态为 \(i\) 并且当前停留在城市 \(j\).
状压的三层循环很清晰,第一层遍历所有状态,第二层遍历 \(2--k+1\) 个城市( \(i\) 状态时停留在 \(j\) ),
第三层遍历 \(2--k+1\) 个城市(把这个城市加入当前状态),此时状态变为 \(dp[i | (1<<(h-2))][h]\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 20005;
int n,m,k,q;
int a[(1<<20)+5];
int dp[(1<<20)+5][21];
struct E
{
int nxt,to,w;
} e[400005];
int head[N],tot;
void add(int u,int v,int w)
{
e[++tot]={head[u],v,w};
head[u]=tot;
}
int d[30][N];
bool v[N];
void dj(int s)
{
memset(d[s],0x3f,sizeof(d[s]));
memset(v,0,sizeof(v));
priority_queue<pair<int,int> > q;
d[s][s]=0; q.push(make_pair(0,s));
while(!q.empty())
{
int u=q.top().second; q.pop();
if(v[u]) continue;
v[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int to=e[i].to;
if(!v[to]&&d[s][to]>d[s][u]+e[i].w)
{
d[s][to]=d[s][u]+e[i].w;
q.push(make_pair(-d[s][to],to));
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,z);
}
for(int i=1;i<=k+1;i++) dj(i);
if(k==0)
{
printf("%d",d[1][n]);
return 0;
}
scanf("%d",&q);
for(int i=1;i<=q;i++)
{
int x,y;
scanf("%d%d",&x,&y);
a[y] |= (1<<(x-2));
}
memset(dp,0x3f,sizeof(dp));
dp[0][1]=0;
for(int i=2;i<=k+1;i++)
{
if(!a[i]) dp[(1<<(i-2))][i]=d[1][i];
}
for(int i=0;i<(1<<k);i++)
{
for(int j=2;j<=k+1;j++)
{
if(!(i & (1<<(j-2)))) continue;
for(int h=2;h<=k+1;h++)
{
if(!(i & (1<<(h-2)))&&(i | a[h])==i)
dp[i | (1<<(h-2))][h]=min(dp[i | (1<<(h-2))][h],dp[i][j]+d[j][h]);
}
}
}
int ans=1e9;
for(int i=2;i<=k+1;i++) ans=min(ans,dp[(1<<k)-1][i]+d[i][n]);
printf("%d\n",ans);
return 0;
}
以上为非AC代码
优化
洛谷很不人道的把空间卡到了 \(64MB\) ,正解为滚动数组,但是不会~~
于是采用玄学做法,在上述代码中我们开了 \((1<<20)\) 的数组存 \(20\) 个城市的状态,
但其实我们判断约束条件时有一个城市是没用的,那就是当前这个城市本身,
所以实际上有用的只有 \((1<<19)\) , 有一位是永远空着的。
所以每次判断时我们把中间这位跳过去(如下图):
int solve(int x, int y)//x表示状态,y表示要跳过的这一位。
{
return (y & ((1 << x - 1) - 1)) + (y >> x << (x - 1));
}
有点绕,简单来说就是把前 \(x-1\) 和 \(x+1\) 到最后一位保留,然后拼接在一起,
#include<bits/stdc++.h>
using namespace std;
const int N = 20001;
int n,m,k,p;
int a[(1<<20)];
int dp[(1<<19)][21];
struct E
{
int nxt,to,w;
} e[400001];
int head[N],tot;
void add(int u,int v,int w)
{
e[++tot]={head[u],v,w};
head[u]=tot;
}
int d[25][N];
bool v[N];
priority_queue<pair<int,int> > q;
void dj(int s)
{
memset(d[s],0x3f,sizeof(d[s]));
memset(v,0,sizeof(v));
d[s][s]=0; q.push(make_pair(0,s));
while(!q.empty())
{
int u=q.top().second; q.pop();
if(v[u]) continue;
v[u]=1;
for(int i=head[u];i;i=e[i].nxt)
{
int to=e[i].to;
if(!v[to]&&d[s][to]>d[s][u]+e[i].w)
{
d[s][to]=d[s][u]+e[i].w;
q.push(make_pair(-d[s][to],to));
}
}
}
}
int solve(int x, int y)
{
return (y & ((1 << x - 1) - 1)) + (y >> x << (x - 1));
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,z);
}
for(int i=1;i<=k+1;i++) dj(i);
if(k==0)
{
printf("%d",d[1][n]);
return 0;
}
scanf("%d",&p);
for(int i=1;i<=p;i++)
{
int x,y;
scanf("%d%d",&x,&y);
a[y] |= (1<<(x-2));
}
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=k;i++)
{
if(!a[i+1]) dp[0][i]=d[1][i+1];
}
for(int i=1;i<(1<<k);i++)
{
for(int j=2;j<=k+1;j++)
{
if((i & (1<<j-2))&&((i & a[j])==a[j]))
for(int h=2;h<=k+1;h++)
{
if(i & (1<<h-2))
dp[solve(j-1,i)][j-1]=min(dp[solve(j-1,i)][j-1],dp[solve(h-1,(i-(1<<j-2)))][h-1]+d[h][j]);
}
}
}
int ans=1e9;
for(int i=2;i<=k+1;i++) ans=min(ans,dp[(1<<k-1)-1][i-1]+d[i][n]);
printf("%d\n",ans);
return 0;
}
总结
根据数据范围找状压内容,对于约束条件比较熟悉的是差分约束的形式,
其实换个思路,约束条件也可以看成状态转移的条件,这样再看状压就合情合理了。