问题描述
大致思路: 最短路+拆点
- 显然,本题是一道与图论当中最短路有关的问题,因此考虑使用SPFA / dijkstra等算法。观察本题数据规模和约定,部分测试数据不存在小道,就转化为了经典的最短路模板题,直接背模板就好了。对于还有一部分测试数据“所有小道不相交”,则分开讨论大小道:表示若当前边为大道,边权=当前边疲惫度;若当前边为小道,则边权=当前边疲惫度的平方,从而又转化为了最短路模板题。这样就可以拿到80分~
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 510,M=200010;//注意,无向边看作是两条相反方向的有向边,因此边数范围需要*2
int h[N],e[M],ne[M],idx;
long long w[M];//注意c的范围是1e5,若c是小边,经过平方为1e10,超过int范围,因此需要long long
int n,m;
long long dist[N];
queue<int> q;
bool st[N];
void add(int a,int b,long long c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int SPFA(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
q.push(1);
st[1]=true;
while(q.size()){
int u=q.front();
q.pop();
st[u]=false;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[u]+w[i]){
dist[j]=dist[u]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return dist[n];
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
int tag,a,b;
long long c;
while(m--){
cin>>tag>>a>>b>>c;
if(tag==1){
add(a,b,c*c);
add(b,a,c*c);
}
else{
add(a,b,c);
add(b,a,c);
}
}
cout<<SPFA()<<endl;
}
- 还有20分,则需要考虑路径当中可能存在的连续小路的情况。这时候就要引入拆点。个人觉得这与dp的状态表示与状态集合划分非常像。
点的表示:dist[i][j]——表示从1号点走到第i号点,路径包含最后一段是连续小道的总长度为j的最短路径
点的划分:根据从1号点走到i号点路径上的最后一条边的类型划分:要么是大道,要么是小道。
(1)大道:很好理解,直接加上最后一条边权即可,注意j=0;
(2)小道:由于连续的小道是需要先求和再求平方,因此该路径的疲劳度需要先减去原来连续小道所花费的疲劳度,再加上(更新后连续小道总长度之和)的平方,注意更新j=j+w
这样,同一个点,由于第二维j的不同,被拆成n个点——拆点。
再应用堆优化版的dijkstra算法,即可求得结果。 - !!!再仔细阅读题目中的答案限制范围,不超过1e6,表明连续小道的长度之和一定不会超过1000,因此dist[i][j]中j的范围为j<=1000,又缩小了范围。
OK~上代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 510, M = 200010,INF=0x3f3f3f3f;
int h[N],e[M],ne[M],w[M],tag[M],idx;
int dist[N][1010];//连续小道的长度之和一定不会超过1000
bool st[N][1010];//点的去重标记也跟随拆点
int n,m;
void add(int t,int a,int b,int c){
e[idx]=b,w[idx]=c,tag[idx]=t,ne[idx]=h[a],h[a]=idx++;
}
struct Node{//拆点,由于y的不同,x号点被拆成若干点
int x,y,d;
bool operator<(const Node &p)const{
return d>p.d;
}
};
//堆优化版dijkstra
void dijkstra(){
priority_queue<Node> heap;//priority_queue默认大根堆,由于Node内置排序为降序,因此实际意义还是小根堆
heap.push({1,0,0});
memset(dist,0x3f,sizeof dist);
dist[1][0]=0;
while(heap.size()){
Node t=heap.top();
heap.pop();
if(st[t.x][t.y])continue;
st[t.x][t.y]=true;
for(int i=h[t.x];i!=-1;i=ne[i]){
int k=e[i],weight=w[i];
if(tag[i]){//小路
if(t.y+weight<=1000)//连续小道的长度之和一定不会超过1000
if(dist[k][t.y+weight]>t.d-t.y*t.y+(t.y+weight)*(t.y+weight)){
dist[k][t.y+weight]=t.d-t.y*t.y+(t.y+weight)*(t.y+weight);
if(dist[k][t.y+weight]<=INF) heap.push({k,t.y+weight,dist[k][t.y+weight]});
}
}
else{//大路
if(dist[k][0]>t.d+weight){
dist[k][0]=t.d+weight;
if(dist[k][0]<=INF) heap.push({k,0,dist[k][0]});
}
}
}
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
int t,a,b,c;
while(m--){
cin>>t>>a>>b>>c;
add(t,a,b,c);
add(t,b,a,c);
}
dijkstra();
int res=INF;
for(int i=0;i<=1000;i++){
res=min(res,dist[n][i]);
}
cout<<res<<endl;
}
一些小Tips
- CSP第3、4、5题心态先稳住,不要着急,仔细阅读数据规模与约定,部分测试数据可能比较简单,先尝试拿分,再努力尝试拿满分;另外,数据规模里也常有答案是否会爆int的提示,注意把类型定义为long long;
- !!!拆点的思想方法(如上所述),并且拆点时,点的去重标记也要跟随拆点;
- SPFA模板注意点:
st[] :标记的是当前队列中的点,入队st[i]=true;出队st[i]=false;每次入队之前需要判断当前队列中是否已有该点,即st[i]是否为true,st[i]为false才入队; - dijkstra堆优化模板的注意点:
(1)st[]:借助堆(小根堆),快速找到当前最短的dist(堆顶元素),将对应的点加入已经过的点的集合st[],再用该点去更新其他点的dist:表明入堆的时候直接push就好了,而堆顶元素出堆时需要把st[i][j]标记为true,再利用堆顶元素去更新dist。
(2)dist[]:记得初始化! - 用邻接表存无向图时:
(1)h[]: 记得初始化!!
(2)M(边数范围):无向图记得翻倍!!