APIO2013 T3 Tasksauthor

首先解决 SSSP 问题的六个点。我们观察给出的三个算法。

Floyd 的运行次数是死的 n3 次,超了就挂,和边连接情况以及询问无关。

BellmanFord 的理论复杂度是 O(nm),每次都要跑 Q 次。

Dijkstra 的算法是 O(mlogn),没什么意义,正权边随便过,负权边不能过的情况需要特殊构造。

(这道题几乎全部的难点在于 test4,需要对 Dijkstra 的一些内容有着深入的理解)

1

test1DijFloyd。凡是卡 Floyd 的,都可以用一个 101 个点,0 条边的空图去卡,最后要有一次询问,就问 01。在这个数据下 DijBf 根本就不工作,随便跑,Floyd 会被卡满。数字个数 104

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<100<<endl;
	rp(i,101)cout<<0<<endl;
	cout<<1<<endl;
	cout<<"1 2"<<endl;
	return 0;
}
//Crayan_r

2

test2FloydBf。首先,点数不能超过 100,就可以放过 Floyd。然后考虑 Bf 怎么卡。我们发现它一共会跑许多轮,每次都是从 0n1 依次更新扫描所有边。那么我们的目标就是把跑的轮数卡满。假设我们需要的最短路是 xy,那么理想状态就是每轮只能沿着最短路更新一次。

那么我们就考虑怎么卡。发现只要下一轮要走的新点编号比原先的点小,就必须下一轮再更新。这样我们连 990 的链,然后随便连一些别的权值很大的边,就可以把复杂度卡到 nm

vt<pii>vv[105];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<100<<endl;
	vv[0].pb({99,1});
	rep(i,2,99)vv[i].pb({i-1,1});
	int cnt=0;
	rep(i,1,99){
		rep(j,i+1,99){
			vv[i].pb({j,999999});
			cnt++;
			if(cnt==950)break;
		}if(cnt==950)break;
	}
	rd(i,100){
		cout<<vv[i].size()<<" ";
		for(auto j:vv[i])cout<<j.first<<" "<<j.second<<" ";
		cout<<endl;
	}
	cout<<10<<endl;
	rd(_,10)cout<<0<<" "<<1<<endl;
	return 0;
}
//Crayan_r

3

test3BfFloyd,直接用第一次的数据即可。

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	freopen("f.out","w",stdout);
	cout<<100<<endl;
	rp(i,101)cout<<0<<endl;
	cout<<1<<endl;
	cout<<"1 2"<<endl;
	return 0;
}
//Crayan_r

4

DijkstraDij 怎么卡?在正权图上 Dij 怎么也不可能跑不过 Floyd,那就只能去打负权边的主意了。

然后看到这个数字个数限制还非常捉急,这么一点数字,每个点还至少要连一条边,匀下来点数只有 40 多,怎么办?卡成平方还不够,只能往指数级上卡。

我们来看 Dij 的复杂度证明,应用了正权边的关键证明就是每个点出堆的时候当前路等于最短路。那么我们就额可以基于这一点进行设计。

image

这就是一个小小的例子,我们可以构造这样的结构,骗过 Dij,让它先走下面的 1,直到走到了终点,再发现堆里还积压着更优的做法。

如果我们把这样的结构重复 k 遍,就可以让堆每次释放最后的点。而且我们需要后面的点先出队,也就是较大的这个边权应该是递减的(或者从 300,就可以将其设置为常数而天然由离终点更近的先出堆)。从而把复杂度卡到 n2。但是这样显然卡不掉。我们需要的是指数级。

指数级意味着什么呢?意味着我们在第一个结构试错过一次走上对路的时候,依然会把后面所有结构的不优答案再试一遍。那又是什么呢?就是我们每次走对路的优势要从终点到起点递增,大到只要我们当前选的是对的,后面的即使全部选错的,也比当前选错后面全对要小。这样前面轮的答案就不会影响到当前的循环。

也就是,假设我们的每个结构是
image

那么 f(x) 是骗答案不走的大边,g(x) 是走对路的收益,那么假设从起点到终点的编号依次是 n,n1,n2,n3,则 g(x) 要大于 i\glxg(i)

我们发现,如果我们设 g(i)2i,就满足这一条件。

然后还有什么要求呢?f(i) 一定要大于所有 t(i) 的和。我们可以把 t(i) 设为 1f(i) 设为 1630 作为起点,保证靠近终点的先出堆),f(i)g(i) 就可以是 16+2i,但是这样麻烦,我们不如设置 f(i)g(i)2i+4,就一定会大于 16 并且收益满足要求。

这样就可以把 Dij 卡出指数级。我们重复这个结构 16 次,一共 33 个点,48 条边。每一次询问都可以卡出 196606 次(所有的结构都是按照 2n 重复的,每一个重复结构都会扫所有点,所以实际执行次数是 (n1)2nn 指结构个数))。循环 10 次就直接卡掉。

vt<pii>vv[305];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<999<<" "<<1501<<endl;
	for(int i=32;i>=2;i-=2){
		int res=i/2;
		vv[i].pb({i-1,1});
		vv[i].pb({i-2,0});
		vv[i-1].pb({i-2,-(1ll<<(res))});
	}
	rd(i,33){
		cout<<vv[i].size()<<" ";
		for(auto j:vv[i])cout<<j.first<<" "<<j.second<<" ";
		cout<<endl;
	}
	cout<<10<<endl;
	rd(_,10)cout<<32<<" "<<0<<endl;
	return 0;
}
//Crayan_r

5

我们直接把 test2 的数据搬过来,但是现在另一个程序换成了 Dij,那就……放飞自我!我们可以随便卡了,因为都是正权边,Dij100 通过的。不过给的数字个数倒是减小了,但并不会影响我们卡死 BellmanFord 的脚步。我们直接造一个 300(极限个数)个点的大链,还剩 37 个边连不连都无所谓了,询问 10 次,然后 BellmanFord 就直接上西天了。

vt<pii>vv[305];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<300<<endl;
	rep(i,1,299)vv[i].pb({i-1,1});
	int cnt=0;
	rep(i,1,299){
		rep(j,i+1,299){
			vv[j].pb({i,999999});
			cnt++;
			if(cnt==37)break;
		}if(cnt==37)break;
	}
	rd(i,300){
		cout<<vv[i].size()<<" ";
		for(auto j:vv[i])cout<<j.first<<" "<<j.second<<" ";
		cout<<endl;
	}
	cout<<10<<endl;
	rd(_,10)cout<<299<<" "<<0<<endl;
	return 0;
}
//Crayan_r

6

直接套 test4 的,三十年河东三十年河西,现在怎么跑都不会挂的轮到我 BellmanFord 啦!!在这么小的点个数,BellmanFord 想挂都不可能。唯一的难点是给的数字数量变少了,我们原来的方案超出了 8 个数。这怎么办?砍点吗?那就铁定卡不掉了。我们发现跑一次其实可以卡到接近 2105,那么跑 10 次其实是多余的。跑 6 次就够了。砍掉 4 次询问,恰好可以通过。

vt<pii>vv[305];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<999<<" "<<1501<<endl;
	for(int i=32;i>=2;i-=2){
		int res=i/2;
		vv[i].pb({i-1,1});
		vv[i].pb({i-2,0});
		vv[i-1].pb({i-2,-(1ll<<(res))});
	}
	rd(i,33){
		cout<<vv[i].size()<<" ";
		for(auto j:vv[i])cout<<j.first<<" "<<j.second<<" ";
		cout<<endl;
	}
	cout<<6<<endl;
	rd(_,6)cout<<32<<" "<<0<<endl;
	return 0;
}
//Crayan_r

7

这个问题只是卡掉这个神秘程序,因为 Gamble1100 通过的。

发现算法强制我们用恰好 1501 条边。

那么我们考虑这个神秘程序的工作,其实就是通过编号在前面的点的安排方式进行剪枝。那么我们让它剪不掉就好了。具体而言,我们在前若干个点压根不进行任何限制,让他跑,然后后面的点施加很强的限制(造一堆奇环或者造一个比较小的完全图),导致根本没有方案可以用。这样在 x=2 的时候,算法就会把前面所有的点的所有填涂方式全部枚举一遍,造一个奇环,每一种都到点即停。

然后我们观察一下下发的程序,发现还有点离谱,只有一次搜索结束之后才会出来判断 counter,虽然不知道死在循环里面能不能过,但是保险起见,把没有任何限制的点设置为前 20 个点,保证搜满 106 次又保证能退出即可。

vt<pii>vv[1005];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<999<<" "<<1501<<endl;
	int cnt=1501;
	rep(i,21,997){
		cout<<i<<" "<<i+1<<endl;
		cnt--;
	} 
	rep(i,21,996){
		cout<<i<<" "<<i+2<<endl;
		cnt--;
		if(!cnt)break;
	}
	return 0;
}
//Crayan_r

8

我们发现 Gamble2 是一定会挂的,那么就让这个程序通过就行了。

(让这个程序通过非常简单,随机一个出来就是了)

还是要找靠谱一点的方法的,具体方法就是构造一个二分图,把前 500 个点和后 499 个点分开,保证每个点都和另一部分连边。这样自然就能一遍跑出答案。因为前面的是默认的 0,后面因为每个都和 0 连边了,第一选择就是 1

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout<<999<<" "<<1501<<endl;
	int cnt=1501;
	rep(i,0,448){
		cout<<i<<" "<<i+450<<endl;;cnt--;
	}cnt--;cout<<449<<" "<<450<<endl;
	rep(i,1,449){
		rep(j,450,998)if(j!=i+450){
			cout<<i<<" "<<j<<endl;cnt--;
			if(!cnt)break;
		}
		if(!cnt)break;
	}
	return 0;
}
//Crayan_r

然后就通过了。

另外,洛谷的 BellmanFord 测试程序似乎和下发的程序并不一样,如果在 2 中只造 98 的链而非 99,在 5 中只造 298 而非 299UOJ 可以通过,但洛谷卡不满。

posted @   jucason_xu  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示