【杂题】[CodeForces 827 F] Dirty Arkady's Kitchen【DP】【最短路】【堆】

33 篇文章 0 订阅
29 篇文章 0 订阅

原题链接:http://codeforces.com/problemset/problem/827/F

Description

给出一张n个点,m条边的无向图,经过每条边所花费的时间均为1
每一条边有一个出现时间区间 [ l , r ] [l,r] [l,r],也就是说,你只能在 [ l , r − 1 ] [l,r-1] [l,r1]这一个时间区间内进入这条边,并且进入就不能回头,只能走到另一个端点。

你在时刻0时在1号点,每一个时刻必须走一条边,不能自己走进死胡同
求抵达n号点最少花费的时间。

n , m ≤ 500000 , l , r ≤ 1 0 9 n,m\leq 500000,l,r\leq 10^9 n,m500000,l,r109

Solution

直接做是很困难的,我们考虑发掘一下性质。

假设时刻0在1号点,我们有一条一直可用的边链接1,2
那么我们肯定能在时刻1,3,5,7,9,…,2k+1的时间走到2

如果我们拆点,将所有点分为偶点和奇点,原来的边拆成偶点与奇点之间的连边,相应修改一下出现区间。

此时一条边所能贡献的就是一段区间了。
我们将所有边按照出现时间排序,用一个堆来维护其中的最小值。
记录 l a s t [ i ] last[i] last[i]表示节点i目前所能停留的最晚时间,此外对于每个点都维护一个边的备用队列。

每次取出堆中的出现时间最短的边,记为 ( x , y ) (x,y) (x,y),如果last[x]大于等于出现时间,那就意味着这条边是可以走的。(因为我们按照奇偶拆点,上一次走过来这一个点的边出现时间一定小于当前点时间)
此时可以更新x,y的last,并可以更新y的最早抵达时间(更新的值就是这条边出现时间+1)

如果这条边不能走,那么塞入这条边的起点的队列备用。
如果某次更新last的时候使得这条边可以使用了,那么修改它的出现时间,并重新塞入堆中,并将其出队。

这样每条边最多只会被计算两次,复杂度是 O ( m log ⁡ m ) O(m\log m) O(mlogm)

Code

#include <bits/stdc++.h>
#define fo(i,a,b) for(int i=a;i<=b;++i)
#define fod(i,a,b) for(int i=a;i>=b;--i)
#define N 500005
using namespace std;
int od[N],ev[N],n1,n,m,m1,a[4*N][4],last[2*N],ans,pt[2*N],sz[2*N];
struct node
{
	int w;
	friend bool operator <(node x,node y)
	{
		return a[x.w][2]>a[y.w][2];
	}
};
priority_queue<node> h;
vector<int> d[2*N];
int lev(int k) {return(k&1)?k+1:k;}
int lod(int k) {return(k&1)?k:k+1;}
int sev(int k) {return(k&1)?k-1:k;}
int sod(int k) {return(k&1)?k:k-1;}
void read(int &x)
{
	x=0;char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
}
void link(int x,int y,int l,int r)
{
	if(l>r) return;
	a[++m1][0]=x,a[m1][1]=y,a[m1][2]=l,a[m1][3]=r;
	h.push((node){m1});
}
void update(int k,int st)
{
	while(pt[k]<sz[k]&&a[d[k][pt[k]]][2]<=last[k])
	{
		int q=d[k][pt[k]];pt[k]++;
		a[q][2]=max(a[q][2],st);
		if(a[q][2]<=a[q][3]) h.push((node){q});
	}
}
int main()
{
	cin>>n>>m;
	if(n==1) {printf("0\n");return 0;}
	fo(i,1,n) od[i]=++n1,ev[i]=++n1;
	fo(i,1,m)
	{
		int x,y,l,r;
		read(x),read(y),read(l),read(r);
		r--;
		link(od[x],ev[y],lod(l),sod(r));
		link(ev[x],od[y],lev(l),sev(r));
		link(od[y],ev[x],lod(l),sod(r));
		link(ev[y],od[x],lev(l),sev(r));
	}
	memset(last,128,sizeof(last));
	last[ev[1]]=0;
	while(!h.empty())
	{
		int w=h.top().w;h.pop();
		if(last[a[w][0]]>=a[w][2])
		{
			if(a[w][1]==od[n]||a[w][1]==ev[n])
			{
				ans=a[w][2]+1;
				printf("%d\n",ans);
				return 0;
			}
			last[a[w][0]]=max(last[a[w][0]],a[w][3]);
			last[a[w][1]]=max(last[a[w][1]],a[w][3]+1);
			update(a[w][1],a[w][2]+1);
		}
		else d[a[w][0]].push_back(w),sz[a[w][0]]++;
	}
	printf("-1\n");
}
区间DP是一种动态规划的方法,用于解决区间范围内的问题。在Codeforces竞赛中,区间DP经常被用于解决一些复杂的字符串或序列相关的问题。 在区间DP中,dp[i][j]表示第一个序列前i个元素和第二个序列前j个元素的最优解。具体的转移方程会根据具体的问题而变化,但是通常会涉及到比较两个序列的元素是否相等,然后根据不同的情况进行状态转移。 对于区间长度为1的情况,可以先进行初始化,然后再通过枚举区间长度和区间左端点,计算出dp[i][j]的值。 以下是一个示例代码,展示了如何使用区间DP来解决一个字符串匹配的问题: #include <cstdio> #include <cstring> #include <string> #include <iostream> #include <algorithm> using namespace std; const int maxn=510; const int inf=0x3f3f3f3f; int n,dp[maxn][maxn]; char s[maxn]; int main() { scanf("%d", &n); scanf("%s", s + 1); for(int i = 1; i <= n; i++) dp[i][i] = 1; for(int i = 1; i <= n; i++) { if(s[i] == s[i - 1]) dp[i][i - 1] = 1; else dp[i][i - 1] = 2; } for(int len = 3; len <= n; len++) { int r; for(int l = 1; l + len - 1 <= n; l++) { r = l + len - 1; dp[l][r] = inf; if(s[l] == s[r]) dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]); else { for(int k = l; k <= r; k++) { dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]); } } } } printf("%d\n", dp[n]); return 0; } 希望这个例子能帮助你理解区间DP的基本思想和应用方法。如果你还有其他问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值