9.2 图论专项四模拟赛小记
A.最短路
原题洛谷指路:P1144 最短路计数
看评测一开始 0pts,死于无向边看成单向边。后来在洛谷提交的时候调了好久好久,死于数组开小。
其他没什么,就是一个 dijkstra 的板子。转移路径的时候,若这条路比当前的路长度小,那这条路的答案就更新为转移过来新路的答案;若这条路和当前路长度一样,那将答案加上新路。
Code P1144
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
const int mod = 1e5 + 3;
int n, m;
int vis[N];
priority_queue< pair<int, int> > q;
int idx, e[N], w[N], nxt[N], head[N];
struct node{int d, tot;}ans[N];
void add(int x, int y, int z)
{
e[++ idx] = y;
w[idx] = z;
nxt[idx] = head[x];
head[x] = idx;
}
void dij()
{
ans[1].d = 0;
ans[1].tot = 1;
q.push(make_pair(0, 1));
while(q.size())
{
int x = q.top().second;
q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i = head[x]; i; i = nxt[i])
{
if(ans[e[i]].d == ans[x].d + 1) ans[e[i]].tot = (ans[e[i]].tot + ans[x].tot) % mod;
else if(ans[e[i]].d > ans[x].d + 1)
{
ans[e[i]].d = ans[x].d + 1;
ans[e[i]].tot = ans[x].tot % mod;
q.push(make_pair(-ans[e[i]].d, e[i]));
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) ans[i].d = 1e9;
while(m -- )
{
int x, y;
scanf("%d%d", &x, &y);
add(y, x, 1);
add(x, y, 1);
}
dij();
for(int i = 1; i <= n; i ++ ) printf("%d\n", ans[i].tot % mod);
return 0;
}
B.灾后重建
洛谷原题指路:P1948 [USACO08JAN] Telephone Lines S
总费用来自最长的路径。即求最大值最小。
一看到最大值最小、最小值最大第一反应就是二分答案。
于是本题我写的是 dijkstra + 二分答案。
难点还是在于 check。设 x 为二分的费用,则要求跑最短路时边权大于 x 的边数 <= k。即对于每一条边,只要边权 > x,就需要使用一个免费电话杆。
这里很巧妙的重新设置边权,需使用免费电话杆的边权为 1,否则为 0。跑一遍 dijkstra,实际上就是统计边权 > x 的边数。
Code P1948
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
const int inf=0x7fffffff;
int n,m,k;
int l,r,ans=inf;
int vis[N],d[N],t[N];
int idx,e[N],w[N],nxt[N],head[N];
void add(int x,int y,int z){
e[++idx]=y;
w[idx]=z;
nxt[idx]=head[x];
head[x]=idx;
}
void dij(){
priority_queue<pair<int, int> > q;
q.push(make_pair(0,1));
for(int i=1;i<=n;i++){
vis[i]=0;
d[i]=inf;
}
d[1]=0;
while(q.size())
{
int x=q.top().second;
q.pop();
if(vis[x]) continue;
vis[x]=1;
for(int i=head[x];i;i=nxt[i]){
if(d[e[i]]>d[x]+t[i]){
d[e[i]]=d[x]+t[i];
q.push(make_pair(-d[e[i]],e[i]));
}
}
}
}
bool check(int x){
for(int i=1;i<=idx;i++)
{
if(w[i]>x) t[i]=1;
else t[i]=0;
}
dij();
return (d[n]<=k) ? 1 : 0;
}
int main(){
scanf("%d%d%d", &n,&m,&k);
while(m -- ){
int x, y, z;
scanf("%d%d%d", &x,&y,&z);
add(x,y,z);
add(y,x,z);
r=max(r,z);
}
while(l<=r)
{
int mid=(l+r)>>1;
if(check(mid))
{
r=mid-1;
ans=min(ans,mid);
}
else l=mid+1;
}
if(ans==inf) puts("-1");
else printf("%d",ans);
return 0;
}
但好像并不需要最短路,可以直接用 bfs + 二分,由于 bfs 是 O(n) 的所以会跑的飞快。所以如果数据范围开大 10 倍,spfa、dij 什么的都会寄掉。
本题还是 bfs 比较强。至于我为什么跟本没想到,主要是平时几乎没写过 bfs,所以在这里也练练。思路是一样的。
Code bfs
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
const int inf=0x3f3f3f3f;
int n,m,k;
int l,r,ans=inf;
int idx,e[N],w[N],nxt[N],head[N];
int vis[N],dis[N];
void add(int x,int y,int z){
e[++idx]=y;
w[idx]=z;
nxt[idx]=head[x];
head[x]=idx;
}
bool check_bfs(int x){
memset(dis,inf,sizeof dis);
dis[1]=0,vis[1]=1;
queue<int> q;
q.push(1);
while(q.size()){
int t=q.front();
q.pop();
vis[t]=0;//回溯
for(int i=head[t];i;i=nxt[i]){
int p=(w[i]>x)?1:0;
if(dis[e[i]]>dis[t]+p){
dis[e[i]]=dis[t]+p;
if(! vis[e[i]]){
vis[e[i]]=1;
q.push(e[i]);
}
}
}
}
if(dis[n] <= k) return 1;
return 0;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
while(m--){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
r=max(r,z);
}
while(l<=r){
int mid=(l+r)>>1;
if(check_bfs(mid)){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
if(ans==inf) puts("-1");
else printf("%d",ans);
return 0;
}
C.卡尔距离
原题:bzoj 4152.The Captain
题意:给平面上 n 个点,定义 (x1,x2) 到 (y1,y1) 的费用为 min(|x1-x2|,|y1-ty2|)。求从 1 号点到 n 号点的最小费用。
本题思路上稍微一点的难度就是建图。图建好了直接跑最短路就好。
画图会发现,当有三个点 i, j, k,且 i 与 j 相邻, j 与 k 相邻时,我们不会把 i 与 k 连起来,而是把 i 与 j、 j 与 k 连起来。这样才会缩短 i 与 k 之间的距离。
于是根据横坐标和纵坐标分别两次排序,每次将相邻的两个点之间连边后跑最短路即可。
Code bzoj 4152
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+10;
const ll inf=0x3f3f3f3f;
int n;
int vis[N];
ll d[N],w[N];
int idx,e[N],nxt[N],head[N];
priority_queue<pair<ll,int> > q;
struct node{ll x,y; int id;}a[N];
void add(int x,int y,ll z){
e[++idx]=y;
w[idx]=z;
nxt[idx]=head[x];
head[x]=idx;
}
bool cmp1(node b,node c){return b.x<c.x;}
bool cmp2(node b,node c){return b.y<c.y;}
void ad(){
}
void dij(){
memset(vis,0,sizeof vis);
memset(d,inf,sizeof d);
d[1]=0;
q.push(make_pair(0,1));
while(q.size()){
int x=q.top().second;
q.pop();
if(vis[x]) continue;
vis[x]=1;
for(int i=head[x];i;i=nxt[i]){
if(d[e[i]]>d[x]+w[i]){
d[e[i]]=d[x]+w[i];
q.push(make_pair(-d[e[i]],e[i]));
}
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lld%lld",&a[i].x,&a[i].y);
a[i].id=i;
}
sort(a+1,a+n+1,cmp1);
for(int i=1;i<=n;i++){
add(a[i].id,a[i+1].id,abs(a[i].x-a[i+1].x));
add(a[i+1].id,a[i].id,abs(a[i].x-a[i+1].x));
}
sort(a+1,a+n+1,cmp2);
for(int i=1;i<=n;i++){
add(a[i].id,a[i+1].id,abs(a[i].y-a[i+1].y));
add(a[i+1].id,a[i].id,abs(a[i].y-a[i+1].y));
}
dij();
printf("%lld",d[n]);
}
D.奶酪 - 学好语文很重要
原题:bzoj 2165.大楼
题意:给定一张有向图,求从 1 号点出发,到达编号 >= m 的点且经过边数最少的路径的边数。
读题的关键在于,对于通道 \((x,y,z)\) 需要注意到,你并不关心这具体是哪一层奶酪,你只需要知道从 x 走到 y 你得到的答案能增加 z。
题意挺绕的,如果没读懂题目的话建议多读几遍,好好理解一下。
注意到 n 很小,最终目标巨大。从题目中抽象出来问题后容易想到做法:倍增 + floyd。
\(f[k][i][j]\) 表示走 \(2^k\) 步以内,从 i 号点到 j 号点可得到的最大答案。矩乘优化,每次 check 一下。
Code bzoj 2165
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N=105;
const int inf=1e18;
ll m,ans;
int T,n;
struct M{
ll a[N][N];
M operator *(const M &b) const{
M res;
memset(res.a,-1,sizeof res.a);
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
if(a[i][k]==-1) continue;
for(int j=1;j<=n;j++){
if(b.a[k][j]==-1) continue;
res.a[i][j]=max(res.a[i][j],a[i][k]+b.a[k][j]);
}
}
}
return res;
}
}f[65],x,y;
bool check(M res){
for(int i=1;i<=n;i++) if(res.a[1][i]>=m) return 1;
return 0;
}
void solve(){
scanf("%d%lld",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
ll t;
scanf("%lld",&t);
f[0].a[i][j]=(t)?t:-1;
}
}
int t=0;
while(t>=0){
if(t>=64){puts("-1"); return;}
f[t+1]=f[t]*f[t];
if(check(f[t+1])) break;
t++;
}
x=f[0],ans=1;
for(int i=t;i>=0;i--){
y=x*f[i];
if(!check(y)){
x=y;
ans+=(1ll<<i);
}
}
printf("%lld\n",ans+1);
}
int main(){
scanf("%d",&T);
while(T--) solve();
}
E.旅行到永久
n 很小所以想到用 floyd。枚举步数会炸,所以倍增 + dp。当 t 足够大时就可以近似的看成答案。
\(f[t][i][j]\) 表示从 i 到 j 走 \(2^k\) 步时能获得的最大答案。转移方程: \(f[t][i][j]=max(f[t−1][i][k]+f[t−1][k][j]*p^{2^{(t-1)}})\)。
Code P4308
#include<bits/stdc++.h>
typedef double db;
using namespace std;
const int N=110;
int n,m,s;
db p,ans;
db a[N],f[35][N][N];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%lf",&a[i]);
scanf("%d%lf",&s,&p);
for(int t=0;t<=33;t++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++) if(i!=j) f[t][i][j]=-1e9;
while(m--){
int x,y;
scanf("%d%d",&x,&y);
f[0][x][y]=a[y]*p;
}
for(int t=1;t<=32;t++){
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(f[t][i][j]<f[t-1][i][k]+f[t-1][k][j]*p) f[t][i][j]=f[t-1][i][k]+f[t-1][k][j]*p;
if(i==s) ans=(ans,f[t][i][j]);
}
p*=p;
if(p<=(1e-9)) break;
}
printf("%.1lf",ans+a[s]);
}
F.货物运输 - 航线与隧道
原题:P3008 [USACO11JAN] Roads and Planes G
如果不考虑被卡的话就是裸的 spfa(dij 不能跑负边),但众所周知,关于 spfa,他死了。所以本题我们可以用 deque 小小的优化一下。每次插入新元素时比较一下,如果比队头大就插后边。
虽然裸的 spfa 有 88pts。这么善良的出题人。
Code P3008
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+2;
const int inf=0x3f3f3f3f;
deque<int> q;
int n,m,k,s;
int vis[N], d[N];
int idx,e[N],w[N],head[N],nxt[N];
void add(int x,int y,int z){
e[++idx]=y;
w[idx]=z;
nxt[idx]=head[x];
head[x]=idx;
}
void spfa(){
memset(d,inf,sizeof d);
d[s]=0;
vis[s]=1;
q.push_back(s);
while(q.size()){
int x=q.front();
q.pop_front();
vis[x]=0;
for(int i=head[x];i;i=nxt[i])
if(d[e[i]]>d[x]+w[i]){
d[e[i]]=d[x]+w[i];
if(!vis[e[i]]){
if(q.size()&&d[e[i]]>=d[q.front()]) q.push_back(e[i]);
else q.push_front(e[i]);
vis[e[i]]=1;
}
}
}
}
int main(){
scanf("%d%d%d%d", &n,&m,&k,&s);
while(m--){
int x,y,z;
scanf("%d%d%d", &x,&y,&z);
add(x,y,z);
add(y,x,z);
}
while(k--){
int x,y,z;
scanf("%d%d%d", &x,&y,&z);
add(x,y,z);
}
spfa();
for(int i=1;i<=n;i++){
if(d[i]!=inf) printf("%d\n",d[i]);
else puts("NO PATH");
}
return 0;
}
正解是根据题目性质缩点 + dij 吧。此坑待填。