迟来的解题报告——noip 2017提高组

题目请去洛谷上找找吧。我不复制粘贴了。

由于差不多有1年了,所以我把6道题全部都重新做了一遍。

所以题解没有看过任何网上的资料……全都是凭借当初的信息构筑起来的。

代码也按照模糊的记忆重写了(部分啦部分=v=)。。

Day1

T1:首先,看出a和b是互质的(虽然当时我并不是马上看出这一点)。

能够被支付的物品价格p满足p=ax+by,其中,x和y都是  非负  (非负!)整数。

那么由简单的数论,我们知道,p能够被表示,意思就是让我们去找出x和y这组解,

而其实,由于a,b互质,所有的p都是可以被表示的(定理),

因此问题不是出在无解,问题是出在解不合法。

刚才说过,x和y要满足非负,但是同时,注意到,x的周期是b,y的周期是a,

这句话的意思是,当p=ax+by时,x变成x+t*b,而y变成y-t*a,t是整数,那么等式仍然成立。

所以解是无穷多组,而且我们可以构造出来。

因此,解不合法,就意味着p=ax+by的无穷多组解,全部满足x,y至少有一个是负数。

考虑临界点就好了:当x,y恰好一正一负时,然后我们令t=1对x,y作变换,使得这种变换让x,y中负的那个变成正的;

矛盾的出现点,就是那个正的变成负的。

比如,x=b-1,y=-1(这个构造的思想,基于我们要让p尽量大,即x和y尽量大)

此时很显然是临界点,p=a*(b-1)+(-1)*a=a*b-a-b

又比如,x=-1,y=a-1,计算出的p显然会相同。

所以答案就是a*b-a-b,注意,爆int。。

这题贴代码没意义把??

T2:

呃。

不知道如何评价,因为就是一道很蠢萌的模拟。

好吧我承认这题我没有重写,因为真的很蠢萌。。2333

T3:

woo,当年让我吃了屎的题(之一)

考试的时候没调对,结果正常小暴力打错了……

事实证明当时确实差那么几个重要的点。

如果仔细去分析题目的话,会发现其实题目的要求是十分清晰的。

因为,所谓的“有无穷组路线”,其实就是0环的情况。

但是单单判断存在0环还不够,我们要判断出:0环是否存在于一条可能在范围内的路径上。

看上去问题复杂了许多,实则不然。

判断0环这点考场上想复杂了,当时我在纠结如何把0环缩起来……

其实如果代码能力强就可以了。。然而。。

其实直接拓扑排序就好了啊= =我tm今天早上一拍头。。woc。。。

好吧,如果想到拓扑了那么所有问题都解了。。。我当时就是tarjan写炸,dp用bfs写的也炸了。。

咳咳,回归正题。

判断0环可以重新构建一张图,只保留0边,那么再用拓扑排序判断是否有环。

如何判断“0环是否存在于一条可能在范围内的路径上”呢?这个比较好想,比如当前点是x,

而我们已经判断出了x存在于某个0环内,

那么只要判断1~x+x~n的两段最短路径之和是否>=1~n最短路径+K就好了。。(好想吧?)

因此,我们还需要正着跑一遍dij,倒着跑一遍dij,预处理出两个dis数组,一切都好啦。。

~所以0环的判断就ok了。接下来就是如何计算数目。

我相信这个dp是不难想的,因为考场上我记得我看完题10s内就手抖着写出了方程……= =

但是就是dp如何运行的问题了。

比方说,我们状态这样设计:f[u][k]表示1~u点,比最短路多了k的方案数。

那么很简单,f[u][(dis[u]+value(u~v))-dis[v]+k]+=f[v][k]

当(dis[u]+value(u~v))-dis[v]+k<=K(题目给出的K)

那么dp的运行顺序是什么呢?这是当时真正卡住我的点。1~n?不行。n~1?也显然不行。

我们对点的遍历存在某一种顺序,但是这种顺序一开始看起来并不清晰。

考场时,我的第一想法是遍历所有边——因为边可以更新dp值。

但是这是一种刷表的方法,我当时便写成了bfs的形式,后面检查的时候发现这个是只有70的。

因此,我尝试着改进,但是没有治到根本。

顺序怎么定?拓扑排序!

首先,我们只需要规定一个顺序,用拓扑排序即可;但是原图中是有环的,点是加不进所有的,

因此,我们人为规定就好了。

由于这种情况存在解,就是说要么没有0环,要么0环不在最短路中,

前者是不是很好的情况呢?但是为了应付后者,我们应当想到,保留最短路就行了!

没错,把最短路上的边全部都留下来,然后再拓扑排序——存在的环我们之后再更新,

在这之前,只需要找出最短路的所有路径。

然后拓扑排序得到一个顺序。这边是我们dp的顺序。

这是正确的,因为dp的基层是f[u][0],而我们这一步相当于一个基层;

后面以k的逐步增加(f[u][1],f[u][2]……)为顺序,因此dp循环中,k在最外层,

然后按照点的顺序遍历所有的边即可;

即使有环,要求循环更新,我们得出的拓扑序也使得最后的更新偏向n,

每一步更新不必多余走环了。

模拟一下样例的话会很清晰。

考场上就是栽在了调试tarjan和那个bfs上……

其实就算写对了也是70分的。。。

没有想到拓扑还是很讨厌的,主要就是dp顺序没理清。

调试失败的最大问题,就是为了省空间。。。呃

因为有4处使用边表的地方,可以把空间缩到使用2个边表。。然后就多调了30分钟= =

说实话,再做一遍耗了100+分钟……果然退化了。。。

欸。

luogu上70分???最后3个点RE了,我不是很清楚。

但是uojAC了,可能我有点细节出了问题,但是按理来说是没毛病的。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5,MAXM=2e5;
int n,m,K,MOD,TT;
int dis1[MAXN+5],dis2[MAXN+5],que[MAXN+5];
int f[MAXN+5][55];
struct Edge{
	int next,to,val;
}E[MAXM+5],Eopp[MAXM+5],tE[MAXM+5];
int elen,eolen,head[MAXN+5],headopp[MAXN+5];
int telen,thead[MAXN+5],in[MAXN+5];
bool vis[MAXN+5];

struct Node{int point,v;};
bool operator <(Node a,Node b){return a.v>b.v;}
bool operator >(Node a,Node b){return a.v<b.v;}
priority_queue<Node,vector<Node> >Q;

void add(int u,int v,int w){
	E[++elen].next=head[u];
	E[elen].to=v;
	E[elen].val=w;
	head[u]=elen;
}
void addopp(int u,int v,int w){
	Eopp[++eolen].next=headopp[u];
	Eopp[eolen].to=v;
	Eopp[eolen].val=w;
	headopp[u]=eolen;
}
void addt(int u,int v,int w){
	tE[++telen].next=thead[u];
	tE[telen].to=v;
	thead[u]=telen;
	in[v]++;tE[telen].val=w;
}

void dijkstra(){
	memset(dis1,100,sizeof(dis1));
	memset(vis,0,sizeof(vis));
	dis1[1]=0;
	Q.push((Node){1,0});
	while (!Q.empty()){
		Node u=Q.top();Q.pop();
		if (vis[u.point]) continue;
		vis[u.point]=1;
		for (int i=head[u.point];i;i=E[i].next){
			int q=E[i].to;
			if (vis[q]) continue;
			if (E[i].val+u.v<dis1[q]){
				dis1[q]=E[i].val+u.v;
				Q.push((Node){q,dis1[q]});
			}
		}
	}
}
void dijOPP(){
	memset(dis2,100,sizeof(dis2));
	memset(vis,0,sizeof(vis));
	dis2[n]=0;
	Q.push((Node){n,0});
	while (!Q.empty()){
		Node u=Q.top();Q.pop();
		if (vis[u.point]) continue;
		vis[u.point]=1;
		for (int i=headopp[u.point];i;i=Eopp[i].next){
			int q=Eopp[i].to;
			if (vis[q]) continue;
			if (Eopp[i].val+u.v<dis2[q]){
				dis2[q]=Eopp[i].val+u.v;
				Q.push((Node){q,dis2[q]});
			}
		}
	}
}
bool ifN1(){
	memset(thead,0,sizeof(thead));
	memset(in,0,sizeof(in));
	for (int i=1;i<=n;i++)
		for (int j=head[i];j;j=E[j].next){
			int q=E[j].to;
			if (!E[j].val) addt(i,q,0);
		}
	int h=0,t=0;
	for (int i=1;i<=n;i++)
		if (!in[i]) que[++t]=i;
	while (h<t){
		int q=que[++h];
		for (int j=thead[q];j;j=tE[j].next){
			in[tE[j].to]--;
			if (!in[tE[j].to]) que[++t]=tE[j].to;
		}
	}
	for (int i=1;i<=n;i++)
		if (in[i]>0 && dis1[i]+dis2[i]<=K+dis1[n]) return 1;
	return 0;
}
void topsort(){
	telen=0;
	memset(thead,0,sizeof(thead));
	memset(in,0,sizeof(in));
	for (int i=1;i<=n;i++)
		for (int j=head[i];j;j=E[j].next){
			int q=E[j].to;
			if (dis1[i]+E[j].val==dis1[q]) addt(i,q,E[j].val);
		}
	int h=0;TT=0;
	for (int i=1;i<=n;i++)
		if (!in[i]) que[++TT]=i;
	while (h<TT){
		int q=que[++h];
		for (int j=thead[q];j;j=tE[j].next){
			in[tE[j].to]--;
			if (!in[tE[j].to]) que[++TT]=tE[j].to;
		}
	}
}
void DP(){
	memset(f,0,sizeof(f));
	f[1][0]=1;
	for (int k=0;k<=K;k++)
		for (int i=1;i<=TT;i++)
			for (int j=head[que[i]];j;j=E[j].next){
				int q=E[j].to;
				if (dis1[que[i]]+E[j].val-dis1[q]+k<=K)
		 	  (f[q][dis1[que[i]]+E[j].val-dis1[q]+k]+=f[que[i]][k])%=MOD;
			}
}
void solve(){
	telen=elen=eolen=0;
	memset(head,0,sizeof(head));
	memset(headopp,0,sizeof(headopp));
	scanf("%d%d%d%d",&n,&m,&K,&MOD);
	int x,y,z;
	for (int i=1;i<=m;i++){
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);addopp(y,x,z);
	}
	dijkstra(),dijOPP();
	if (ifN1()){printf("-1\n");return;}
	topsort(),DP();
	int ans=0;
	for (int i=0;i<=K;i++) (ans+=f[n][i])%=MOD;
	printf("%d\n",ans);
}
int main(){
	int T;cin>>T;
	while (T--) solve();
	return 0;
}

(诶哟卧槽dij都不太会写了)

Day2:

T1:

哈哈哈,这题爆炸实在是骚。

这种并查集不是一眼就看出来了吗???

这种算法不是一大堆吗??

然后嘛……漏了个洞洞穿天破地的情况

就跪了。

no meaning纯属eat shit请无视我。

还是day1T1适合我=_=

T2:

恩很好这题一定要重写。

最遗憾的题!(虽然只差了30分但是。。。)

状压dp太过显然,问题就是我写错了然后最后关头才发现= =嗯。

至于在哪里写错的,我早已经不记得了。所以还是从头到尾重新想一遍。

(其实是不难的)

首先,题目的难点,主要在于价格的计算。

因为目标是构造一棵生成树,所以价格和这棵树的形状是有关系的。。(因此不一定是最小生成树)

假如我们用DP[state]来表示一种状态(state是二进制状态)

那么可以考虑到,在state的基础上,找出k(k不包含在state内),

然后将这个k接到state上即可。

这个需要如何来实现呢?很简单,用DP[state][depth],加上一维来表示深度即可。

注意,在找k的过程中,我们需要枚举k接在哪一个结点上

设这个结点为p,则p一定在state中。

同时需要枚举深度,这样子的话接入后就可以转移了。

DP[k][depth]=min(DP[p][depth-1]+dis(p,k))

其中,dis(p,k)表示p,k间的距离。

当然,p和depth之间需要满足一些条件,因为一个p可能不会满足任意深度,

所以有些深度是不合法的。但是深度又取决于根——

所以我们需要枚举一个根。也就是枚举地面穿洞处。

这样子的话,只要p到根的距离存在此深度(小细节,有时候可以不处理),就可以转移。

值得注意的是,depth代表的是当前点的深度,

如果弄错了,可能出现一个很讨厌的问题:你发现新的一个节点接入时,

找到了一个最优解,深度是3;但实际上接在深度是2的次解上会更优。

所以枚举深度是很必要的!

分析一下复杂度吧。DP数组可以暴力三维,也可以直接滚动了2维就够了的。

所以空间O(n*2^n)

时间上,枚举根,O(n);枚举状态O(2^n),枚举状态中的点O(n)枚举状态外的点O(n)

因此时间复杂度O(n^3*2^n),n=12,完全可以接受。。

至于实现形式,由于其实有很多状态实际上是不会接触到的

(因为状态中必须包含根,这样子就一下子省略了2^(n-1))

所以完全可以用BFS的形式去扩展状态(我认为这样更好理解,和DFS一样是比较本质的)。

另外,输入数据中,很明显点只有12个,

但是边可以多达1000条。

显然是有重边的,因此边权需要取最小值。当然这个属于明显的细节。。。

代码还在调试中!(考试时也是这样……

即使思想清晰明显,代码实力还是摆在那里)

所以把。。。开学后马上来把坑填了!

(代码还不对需要调。。挂着开学后更QAQ……但是基本是对的了(去年考试后同学间早有过确认))

T3:

好家伙……考场上都没有细想过。。。

都是栽在T2上。。。

挂着!想好思路后验证,代码应该要开学后更新了。。

猜你喜欢

转载自blog.csdn.net/ThinFatty/article/details/81612443