NOIP2017小结

NOIP2017小结

Day1

T1

几乎就是一道小学奥数题,然而我并做不来。但是可以用一些奇奇怪怪的方法过掉。

\(60-100pts\)

这个,我局限于考虑如何\(O(1)\)的判断一个数字\(n\)能否用\(a,b\)表示,所以就始终不能突破\(O(a*b)\)的时间复杂度。但是,可以通过一些鬼畜的概率算法来搞过,比如说,我把从\(a*b\)倒着数,最后\(1000000\)个长度为\(10000\)的区间,每个区间随机选择\(50\)个数字,\(O(1)\)判断一下,然后都过就扫下一个区间,如果有不能表示的,就此终止,然后再从这里开始,往回扫\(1000000\)个长度为\(500\)的区间,每个区间随机选择\(50\)个数字,然后\(O(1)\)判断一下,如果都过就扫下一个区间,如果有不能表示的,就此终止(这里就有一定的可能会舍弃最优解),然后倒着往先前的起点暴力扫,找到一个就直接输出结果。
这么做很大程度上取决于每个区间随机选择的\(50\)个数字是否可以很强。但是,很幸运的是,结果就是\(a*b-a-b\),所以如果我从\(a*b\)倒着扫的时候,基本上是一定可以扫到\(a*b-a-b\)这一个点的,那么这个搞就得到了正确结果,只是时间效率比较慢。。
考虑一下\(O(1)\)判断吧,其实用\(exgcd\)预处理稍微推一下式子,就能很容易得出做法。

余数法

这个方法,很奇怪,不知道我那些同学怎么想到的(其实是我太弱了
就是,我们不妨设\(a>b\),那么我们把\(a\)的倍数去\(mod\) \(b\),就一定可以得到\(b\)个余数,当然,我们很容易知道\(a*b=a*0(mod\) \(b)\),然后\(a*(b+1)=a*1(mod\) \(b)...\),也就是说,我们其实只需要考虑这几个倍数:\(a*0,a*1,a*2,a*3,...,a*(b-1)\),那么这里讨论一个特殊的\(x=a*k(0\le k<b)\),对于每一个大于\(x\)的,并且其\(mod\) \(b\)的余数等于\(x\) \(mod\) \(b\)的余数的数就一定可以被表示出来,对于每一个小于\(x\)的,并且其\(mod\) \(b\)的余数等于\(x\) \(mod\) \(b\)的余数的数就一定不可以被表示出来,并且最大的就是\(x-b\),所以在这些倍数中,\(x-b\)最大的就是\(a*(b-1)-b=a*b-a-b\)

Exgcd法

T2

这就是一个大模拟码农题哈,搞个单调栈先检查一遍是否存在语法错误,至于变量重名问题,搞个\(bool\)数组判重就好了,然后至于计算次幂的问题,在单调栈搞的时候,可以把每个\(F\)对应的\(E\)的映射先预处理好,然后用\(dfs\)搞就好了,并列的取最大值。把一些输入稍微留意的处理一下就好了。

T3

Day2

T1

还是比较简单的一道题吧。弄个两点间距离公式,建图然后跑\(dijsktra\)或者直接用并差集搞一搞就好了。这题的话,好像会被卡\(sqrt\)的精度问题,所以开\(long long\)然后不等式两边都平方就好了。然后的话,在有些变态数据中似乎还会被卡\(long long\)所以搞个移项就好了。

T2

其实是一个很弱的状压\(dp\)啊,但是考场上写的时候,没想到用\(dfs\)的形式去写\(dp\),那酸爽。。最后还没写对。。

\(40pts\)

这个部分,直接搞一个最小生成树就差不多了,因为所有的边的长度都是相等的。或者也可以枚举一下起点,然后暴力跑\(dijsktra\)也应该差不多。但是,只拿了\(30\)分,莫名其妙的。

\(70pts\)

这个部分,其实也是很简单的,直接搞一个\(n^n\)的暴力也就差不多了。也就是枚举每个点是由哪些点转移来的。或者可以搞打通顺序的全排列,然后就是一个贪心选取模拟\(prim\)了呵。
效率就是\(O(n!)\)

\(70-100pts\)

因为全排列会超,所以我们可以搞个随机算法,也就是弄\(10^6-10^7\)个随机排列,然后贪心搞一搞,因为\(8!<<10^6\),所以\(70\)的基础分还是有的,剩下的\(30\)分就看人品了。

\(100pts\)

网上好像有各种诡异的做法,不过有一个做法最直接最简洁,效率也是很好的,大概是\(O(2^n*n^3)\),其实我们直接写\(dp\)写不出来,写出后效性的原因就是我们无法确定\(dp\)的顺序,那么这就有很直接的套路:在\(dfs\)上写\(dp\)。这里用不上记忆化,所以也就不是记忆化搜索了。对于一个状态,我们通过\(dfs\)同时计算距离和\(dp\)值,然后,就很好写,然后,就没啦Σ(⊙▽⊙"a。
好像还有人模拟退火A掉了(什么乱七八糟的东西

参考程序

#include<cstdio>
#include<algorithm>
#include<cstring>
#define rint register int
using namespace std;
const int N=15;
int d[N][N],dis[N],dp[1<<N];
int ans,n,m,oo;
inline bool in(rint k,rint S){
	return S&(1<<(k-1));
}
inline void dfs(int S){
	for (rint i=1;i<=n;++i)
		if (!in(i,S)){
			for (rint j=1;j<=n;++j)
				if (in(j,S) && d[j][i]!=oo){
					rint tt=dis[j]+1;
					if (dp[S|1<<(i-1)]>dp[S]+tt*d[j][i]){
						rint t1=dis[i];
						dis[i]=tt;
						dp[S|1<<(i-1)]=dp[S]+tt*d[j][i];
						dfs(S|1<<(i-1));
						dis[i]=t1;
					}
				}
		}
}
int main(){
	freopen("treasure.in","r",stdin);
	freopen("treasure.out","w",stdout);
	scanf("%d%d",&n,&m);
	memset(d,0x3f,sizeof(d));
	oo=d[0][0];
	for (rint i=1;i<=m;++i){
		rint u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		d[u][v]=min(d[u][v],w);
		d[v][u]=d[u][v];
	}
	rint ans=oo;
	for (rint i=1;i<=n;++i){
		memset(dp,0x3f,sizeof(dp));
		memset(dis,0x3f,sizeof(dis));
		dis[i]=0;dp[1<<(i-1)]=0;
		dfs(1<<(i-1));
		ans=min(ans,dp[(1<<n)-1]);
	}
	printf("%d",ans);
	return 0;
}

T3

题意十分的简洁,就是需要弄个数据结构去维护一个矩阵中,删除一个位置,然后把这一行左移一个单位,再把最后一列前移一个单位。反正,我一看就觉得是线段树,直接建\(n+1\)个线段树,就可以直接搞了,二看就觉得空间分分钟炸掉。于是就没啥办法了。

\(30pts\)

考虑\(30pts\)的数据,暴力模拟就好了。

\(50pts\)

然后还有一个\(20pts\)的数据,\(q\)比较小,似乎有一种\(q^2\)的做法,反正我是不会。
但是后面还有一个\(20pts\)的数据,保证\(n=1\),那么就好办了,直接暴力维护一个线段树,感觉会被卡常,我就写了个树状数组。其实就是这么维护的,我维护了2个树状数组,然后,一个维护现在这个位置的原编号,一个维护前缀和。然后删除一个点,就是把两个树状数组都弄个后缀修改,最后再在最后一个位置加回来就好了,一个简单的差分思想。也就是说,我们把删除的那个位置直接空余出来,然后再最后的位置上再添加信息,貌似2016年的那个蚯蚓,我考场上也是用线段树这么写的,好套路的东西啊

\(60pts\)

既然可以维护一个\(n=1\)的特殊情况,那么\(x=1\)的,就弄4个树状数组就好了,分别是维护第一行的标号与前缀和,维护最后一列的标号与前缀和,但是有点麻烦,而且只有10分,考场上我又没时间写了。

1.把第一行的这个位置删掉,输出结果,然后把最后一列的第二行加进来。
2.把最后一列的第一行删掉,把输出的结果加到最后一列的最后一行。

可能用树状数组写的真有点麻烦,所以可以直接建2个线段树,那就好写一点了。

\(60-100pts\)

这仅仅是个思路,尽管我在想到树状数组后马上就想到了,但是由于思路很混乱,我并没敢在考场上写。
考虑一下我们在\(60\)分做法中尽管效率\(O(qlogn)\)是可以过的,但是我们的做法扩展到\(n*m\)上来就被卡空间了,所以我想了一个这种的办法:暴力分块(阔怕
我们可以对于一行,把\(\sqrt{n}\)列化作一块,然后对于最后一列,再把\(\sqrt{n}\)行化作一块,对于一个块,我们维护2个信息:块的大小和块内的前缀和。
然后删去一个点时,我们找到这一行,然后再这一行中,用二分查找确定这个点所在的块,直接把这个点删掉,然后把最后一列的对应行的加到这一行的最后一个块中,然后把这个点加到最后一列的最后一个块中。
等等,貌似还是会被卡空间哈。因为我们没有能避免建出一个\(n*m\)的矩阵。
但是,把这个思路用到线段树中,再加一些技巧就可以A了,那就是\(100\)分做法了。

\(100pts\)

基本原理

首先有线段树做法:
线段树的问题,常常都很容易被卡空间,然后就要弄一个叫动态开点的思想,就是避免一开始把所有的数据全部都建到线段树中,而是对于每次操作,都单独进行一个点修改,同时据此建一个大小为\(logn\)的线段树,这好像也就是主席树的基本思想哈,严格来说应该叫可持久化啊,但是这也就有麻烦了,因为如果我们在这道题中不一开始就把线段树建好,我们就存在2个麻烦:

1.我们没有办法确定一个点在某一行中线段树中的具体位置,因为我们没有一开始就建好线段树,所以我们没有办法直接维护编号。
2.我们没有办法确定一个点的数值,因为我们没有一开始就建好线段树,所以我们没有办法维护一开始的数值(好熟悉的一句话),那么经过操作后就更没有办法维护了。

那么我们就单独考虑这两个麻烦。

我们沿用分块中维护块的大小的思想,我们在这个动态的线段树中维护一个\(size\)域,用来表示这个区间中存在的点的个数然后二分查找一下,确定询问点的位置。然后考虑一下如何在动态开点中维护一个\(size\)域。其实仔细想一下也是很简单的,如果我们这个区间之前一直没有访问过,说明这个区间内的点的个数是没有受到前面操作的影响的,那么点的个数就是区间长度了,如果这个区间是访问过得,那么我们在做删除操作的时候,\(size-1\),在做添加操作时,\(size+1\)不就好了?需要注意一个细节,就是我们建的线段树应该是\(max(n,m)+q\)大小的啊,那么我们可能存在一开始就没有访问的区间,其右端点是大于\(max(n,m)\),那么在计算初始\(size\)域的时候,就不是区间长度了,而应该这么判断:如果其左端点小于\(max(n,m)\)那么初始的\(size\)域就是\(m-l\),如果其左端点大于\(max(n,m)\)那么初始的\(size\)域就是\(0\),这么做可能就会存在一个问题,也就是说,如果我们在\(n+1\)的位置添加了一个点,那么\(n+2\)\(size\)域在初始化的时候是不是就会出现问题?庆幸的是,这是不会出现的。因为如果\(n+1\)\(n+2\)是一个区间内的,那么这个区间就是访问过得了,那么\(size\)直接加加减减就行了,如果不是一个区间内的,那么\(n+2\)显然是\(0\)啊,因为我们维护的是区间内的总和,而不是整个的前缀和。因为如果维护整个的前缀和,我们在修改时,就避免不了\(O(n)\)的修改。这个麻烦就差不多了。

然后我们考虑一下具体数值的转移,在有上面的做法基础上,我们只需要在size域初始化的同时,直接搞一个赋为(x-1)*m+y就好了啊,然后单独弄个modify过程就好了,似乎就很简单了哈。

Tips

做法可能有点绕啊,所以考场上几乎想不出来呵,即使想出来了,也不一定码的出来呵。所以要想理好思绪再写。

Some expand

然后,好像有一个更好写,更高大上的做法(但是我太弱了,并不会写)——平衡树做法:
搞一个非旋treap(好像也是可持久化数据结构啊)或者就是splay去做类似线段树的操作,那样可以节省一点空间,但是常数就很玄学了,尤其是对于这类卡常变态级的题目,Luogu的机子可以过,但是觉着CCF的老爷就很蛋疼了哈。

参考程序:

#include<cstdio>
#include<algorithm>
#include<cstring>
#define rint register int
using namespace std;
typedef long long ll;
const int M=1e7+3;
const int N=3e5+5;
struct pdt{
	int L,R,sum;
	ll val;
}T[M];
int n,m,q,totn,sz,flag;
int root[N],tot[N];
inline int read(){
	rint x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while('0'<=ch&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return x*f;
}
inline int getSum(int l,int r){
	if (flag>n){
		if (r<=n)return r-l+1;
		if (l<=n)return n-l+1;
		return 0;
	}
	if (r<m)return r-l+1;
	if (l<m)return m-l;
	return 0;
}
inline ll query(int&now,int k,int l=1,int r=totn){
	if (!now){
		now=++sz;
		T[now].sum=getSum(l,r);
		if (l==r){
			if (flag>n) T[now].val=(ll)l*m;
			else T[now].val=(ll)(flag-1)*m+l;
		}
	}
	--T[now].sum;
	if (l==r)return T[now].val;
	rint mid=(l+r)>>1,tt=T[now].L?T[T[now].L].sum:(mid-l+1);
	if (k<=tt)
		return query(T[now].L,k,l,mid);
	else
		return query(T[now].R,k-tt,mid+1,r);
}
inline void modify(int&now,int k,ll v,int l=1,int r=totn){
	if (!now){
		now=++sz;
		T[now].sum=getSum(l,r);
		if (l==r)T[now].val=v;
	}
	++T[now].sum;
	if (l==r) return;
	rint mid=(l+r)>>1;
	if (k<=mid) modify(T[now].L,k,v,l,mid);
		else modify(T[now].R,k,v,mid+1,r);
}
int main(){
	freopen("phalanx.in","r",stdin);
	freopen("phalanx.out","w",stdout);
	n=read(),m=read(),q=read();
	totn=max(n,m)+q;
	for (rint i=1;i<=q;++i){
		ll ans;
		rint x=read(),y=read();
		if (y==m) flag=n+1,ans=query(root[n+1],x);
			else flag=x,ans=query(root[x],y);
		printf("%lld\n",ans);
		flag=n+1,modify(root[n+1],n+(++tot[n+1]),ans);
		if (y!=m){
			flag=n+1,ans=query(root[n+1],x);
			flag=x,modify(root[x],m-1+(++tot[x]),ans);
		}
	}
	return 0;
}
posted @ 2017-11-25 11:42  Camysj  阅读(347)  评论(0)    收藏  举报