程序员人生 网站导航

【BZOJ3669】NOI2004-魔法森林(神奇的解法)

栏目:互联网时间:2015-04-14 07:58:32

在1个魔法森林中,有n个节点(n<=50000),m条边(m<=100000),每一个节点有两个值ai,bi,1<=ai,bi<=50000。有1个精灵要从节点1到达节点n,1个节点i可以经过的要求是它携带的两个值A,B可满足A>=ai,B>=bi,求min(A+B)。
本题目的标准解法是LCT(link-cut-tree),这里讨论1种基于搜索算法的解决方法,其编程复杂性和理解难度略优于LCT做法。
如果每一个节点只有1个值ai,则本题是1道标准的简单动态计划:
dp[i]=max(min(dp[j]),ai) map[i][j]=1
可使用spfa或其他最短路算法实现。当每一个节点的值从1个变成2个时,最容易想到的做法是,枚举其中1个值A,然后用spfa求最小的B,利用A+B更新答案。代码实现以下:

#include <iostream> #include <stdio.h> #include <string.h> #include <set> #include <vector> #pragma comment(linker, "/STACK:102400000,102400000") using namespace std; int n,m,i,j,a,b,best=100001; struct nod{ int nex,a,b; }; vector<nod> lin[50005]; int dp[50001],q[50005]; bool vis[50001]; int ans[50001]; int spfa(int a) { if (ans[a]!=0) return ans[a]; memset(vis,0,sizeof(vis)); memset(dp,-1,sizeof(dp)); dp[1]=0; int head,tail; head=tail=1; q[1]=1; vis[1]=1; int now,xia,b; nod nex; while (head<=tail) { now=q[head%50003]; vis[now]=0; for (int j=0;j<lin[now].size();j++) { nex=lin[now][j]; xia=nex.nex; if (nex.a>a) continue; b=max(dp[now],nex.b); if (dp[xia]==-1 || b<dp[xia]) { dp[xia]=b; if (vis[xia]==0) { vis[xia]=1; tail++; q[tail%50003]=xia; } } } head++; } if (dp[n]==-1) ans[a]=-1; else ans[a]=dp[n]+a; if (dp[n]==-1) return -1; return dp[n]+a; } int main() { //freopen("1.txt","r",stdin); memset(ans,0,sizeof(ans)); scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) lin[i].clear(); nod temp; for (int k=1;k<=m;k++) { scanf("%d%d%d%d",&i,&j,&a,&b); //cout<<i<<' '<<j<<endl; if (i==j) continue; temp.nex=j; temp.a=a; temp.b=b; lin[i].push_back(temp); temp.nex=i; lin[j].push_back(temp); } if (spfa(50000)==-1) { cout<<-1<<endl; return 0; } for (int i=1;i<=50000;i++) { int temp=spfa(i); if (temp!=-1) best=min(best,temp); } cout<<best<<endl; }

这个做法很容易想到,但极限情况下需要做50000次spfa,只能得到25分,需要斟酌其是不是有优化点。(提交记录见http://uoj.ac/submission/4830)
优化1:
在起初时,需要枚举的区间是[1,50000]中的每个A,假定在A=25000时,B=15000。终究答案ans必定满足ans<=40000,因此,A 在[40000,50000]这个区间不可能产生最优解,可以迅速剪去。同理,假定当前最优解best是30000,由于当A<=25000时,满足条件A的边数会比25000时有所减少,B必定会满足B>=15000, 因此,当A在[30000⑴5000,25000]=[15000,25000]这个区间时,也不可能产生最优解,可以剪去。
利用这个思路,可使用dfs的思想去依照中点的顺序枚举每个节点,思路以下:
1.搜索区间(l,r),首先对中点mid求其spfa后的结果temp,并更新全局当前最优解best。
2.搜索区间(l,min(mid,best-(temp-mid)))。
3.搜索区间(mid+1,min(r,best))。
实现的进程中需要注意使用dp[i]记录每个spfa(i)的值,避免重复运算。

int dfs(int l,int r) { //cout<<l<<' '<<r<<' '<<best<<endl; if (l==r) { int temp=spfa(l); if (temp!=-1) best=min(best,temp); return 0; } if (l>r) return 0; int mid=(l+r)/2; int temp=spfa(mid); if (temp==-1) { dfs(mid+1,r); return 0; } best=min(best,temp); //左端点 ll+temp //A的上限 int lef=best-(temp-mid); dfs(l,min(lef,mid)); dfs(mid+1,min(r,best)); return 0; }

加上这个优化后,程序的效力有显著的提高,可以得到50分。(提交记录见http://uoj.ac/submission/4831)
优化2:
在spfa的进程中,会枚举每一个节点i的每条边,由于存边的时候是杂乱无序的,因此只能枚举每一个节点所有的边。为了优化枚举的进程,我们可以将每一个节点对应的边依照a的值从小到大排序,在spfa(a)的进程中,1旦枚举到某1个大于a的边时,就break掉,缩小的枚举的量。这个优化是针对spfa实现进程中的1个优化,但是效果显著,可以得到70分。(提交记录见http://uoj.ac/submission/4834)
优化3:
使用了spfa算法实现,当程序效力出现问题时,可以斟酌spfa算法的两个优化,本处只斟酌其中1种优化。当加入队尾的节点的距离比当前队首节点的距离大时,交换两个节点在队列中的位置。
if (dp[q[(head+1)%50003]]>dp[q[tail%50003]])
{
swap(q[(head+1)%50003],q[tail%50003]);
}
加上这个优化,本题已获得97分,耗时3858ms,通过了NOI比赛时的所有正式数据,OJ附加数据中1组超时。(提交记录见http://uoj.ac/submission/4833)
优化4:
当A变化时,B随之变化的图象其实不是连续的,而是1些离散的点,例如:
当A=1,2,3…20时,B=10000,
当A=21,22,23…30时,B=9500,

即:当A变化时,B会在某些点产生突变,而不是随着A的变化连续变化。
那末,对1个搜索区间[l,r],如果spfa(l)==spfa(r),则不需要对这个区间进行枚举了。
在以上基础上利用此优化,本题中获得了97分,耗时2063ms,在之前的基础上效力得到了显著提升。(提交记录见http://uoj.ac/submission/4849)
优化5:
在搜索算法不管如何也不能在有限时间求出结果时,可以采取卡时的策略,在程序行将超过时间限制时,停止运算,将当前最优解输出,有1定几率得到正确的结果。
修改代码提交后,本题终究获得了100分,并通过了附加的多组数据,总耗时1758ms,在(提交记录见http://uoj.ac/submission/4850)
实际上,本题目在此基础上还有很多优化,例如:对A,B进行离散化,缩小枚举的范围;使用spfa算法的两个优化;把spfa算法改成最小生成树等。都可使效力得到提升,请读者自行尝试。

/* AUTHOR:aqx PROG:魔法森林 LANG:c++ */ #include <iostream> #include <stdio.h> #include <string.h> #include <set> #include <vector> #include <algorithm> using namespace std; int cnt=0; int n,m,i,j,a,b,best=100001; struct nod{ int nex,a,b; }; vector<nod> lin[50005]; int dp[50001],q[50005]; bool vis[50001]; int ans[50001]; int cmp(nod x,nod y) { return x.a<y.a; } inline int spfa(int a) { if (ans[a]!=0) return ans[a]; memset(vis,0,sizeof(vis)); memset(dp,-1,sizeof(dp)); dp[1]=0; int head,tail; head=tail=1; q[1]=1; vis[1]=1; int now,xia,b; nod nex; while (head<=tail) { now=q[head%50003]; vis[now]=0; if (dp[n]!=-1 && dp[now]>=dp[n]) { head++; continue; } for (int j=0;j<lin[now].size();j++) { nex=lin[now][j]; xia=nex.nex; if (nex.a>a) break; b=max(dp[now],nex.b); if (dp[xia]==-1 || b<dp[xia]) { dp[xia]=b; if (vis[xia]==0) { vis[xia]=1; tail++; q[tail%50003]=xia; if (dp[q[(head+1)%50003]]>dp[q[tail%50003]]) { swap(q[(head+1)%50003],q[tail%50003]); } } } } head++; } if (dp[n]==-1) ans[a]=-1; else ans[a]=dp[n]+a; if (dp[n]==-1) return -1; return dp[n]+a; } int dfs(int l,int r) { cnt++; if (cnt>1000) return 0; //cout<<l<<' '<<r<<' '<<best<<endl; if (l==r) { int temp=spfa(l); if (temp!=-1) best=min(best,temp); return 0; } if (l>r) return 0; int mid=(l+r)/2; int temp=spfa(mid); if (temp==-1) { dfs(mid+1,min(r,best)); return 0; } best=min(best,temp); //左端点 ll+temp //A的上限 int lef=best-(temp-mid); if (spfa(l)-l!=spfa(mid)-mid) dfs(l,min(lef,mid)); else { int temp=spfa(l); if (temp!=-1) best=min(best,temp); } if (spfa(r)-r!=spfa(mid)-mid) dfs(mid+1,min(r,best)); return 0; } int main() { int zuida=0; //freopen("1.txt","r",stdin); memset(ans,0,sizeof(ans)); scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) lin[i].clear(); nod temp; for (int k=1;k<=m;k++) { scanf("%d%d%d%d",&i,&j,&b,&a); //cout<<i<<' '<<j<<endl; if (i==j) continue; zuida=max(zuida,a); temp.nex=j; temp.a=a; temp.b=b; lin[i].push_back(temp); temp.nex=i; lin[j].push_back(temp); } for (int i=1;i<=n;i++) { sort(lin[i].begin(),lin[i].end(),cmp); } if (spfa(50000)==-1) { cout<<-1<<endl; return 0; } dfs(1,zuida); cout<<best<<endl; }
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐