[POI2007] ATR-Tourist Attractions

[POI2007] ATR-Tourist Attractions

题目背景

English Edition

题目描述

给出一张有 \(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;
}

总结

根据数据范围找状压内容,对于约束条件比较熟悉的是差分约束的形式,

其实换个思路,约束条件也可以看成状态转移的条件,这样再看状压就合情合理了。

posted @ 2024-04-16 10:59  ppllxx_9G  阅读(12)  评论(0编辑  收藏  举报