NOIP2018题解

Preface

联赛结束后趁着自己还没有一下子把题目忘光,所以趁机改一下题目。

没有和游记一起写主要是怕篇幅太长不美观。

因此这里我们直接讲题目,关于NOIP2018的一些心得有趣的事详见:NOIP2018游记


铺设道路

SB到我怀疑人生,难度比小凯的疑惑还低了不知道几个数量级那题我貌似不是很会证

考场上为了对拍首先写了个暴力分裂区间的贪心,然后一发过了大样例???

想往数据结构上想,但是考虑这是Day1T1,所以肯定有玄学做法。

积木大赛的原题我是做过的,但是考场上忘了,所以YY了一个循环处理单调区间的做法。

主要是找出所有凹陷的点,然后在这些位置断开,循环处理整个区间的贡献就好了。

出来后发现这个和那个差分的做法就十分的像了,但是也不难想。考场上暴力+正解+拍花了十来分种吧。

CODE

#include<cstdio>
#include<cctype>
#define RI register int
using namespace std;
const int N=100005;
int n,a[N],lst,pos; long long ans;
class FileInputOutput
{
    private:
        #define S 1<<21
        #define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
        char Fin[S],*A,*B;
    public:
        FileInputOutput() { A=B=Fin; }
        inline void read(int &x)
        {
            x=0; char ch; while (!isdigit(ch=tc()));
            while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
        }
        #undef S
        #undef tc
}F;
inline int max(int a,int b)
{
    return a>b?a:b;
}
inline void solve(int l,int r,int lst)
{
    int t; for (RI i=r;i>=l;--i) t=max(0,a[i]-lst),lst+=t,ans+=t;
}
int main()
{
    //freopen("road.in","r",stdin); freopen("road.out","w",stdout);
    RI i; for (F.read(n),i=1;i<=n;++i) F.read(a[i]);
    for (i=pos=1;i<=n;++i) if (a[i]>a[i-1]) solve(pos,i-1,lst),pos=i,lst=a[i-1];
    return solve(pos,n,lst),printf("%lld",ans),0;
}

货币系统

这题一眼小凯的疑惑plus,但是转念一想Day1T2不会很难,就往简单像。

首先猜个看上去很对的结论,新的货币系统肯定是用原来的系统中的货币来表示是最优的。

然后来了思路,贪心地把数排序一边,然后无法表示的肯定要选,然后考虑怎么组合出其它的数。

裴楚定理??,EXGCD??,拉到下面看到数据范围\(a_i\le25000\)我才意识到这就是个完全背包

十几分钟写完,一发过了大小样例,拍都懒得拍,心情舒畅。

CODE

#include<cstdio>
#include<cctype>
#include<algorithm>
#define RI register int
using namespace std;
const int N=105,MAX_S=25005;
int t,n,a[N],ans,mx;
class FileInputOutput
{
	private:
		#define S 1<<21
		#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
		char Fin[S],*A,*B;
	public:
		FileInputOutput() { A=B=Fin; }
		inline void read(int &x)
		{
			x=0; char ch; while (!isdigit(ch=tc()));
			while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
		}
		#undef S
		#undef tc
}F;
class DP_Solver
{
	private:
		bool f[MAX_S];
		inline void expand(int x)
		{
			for (RI i=x;i<=mx;++i) f[i]|=f[i-x];
		}
	public:
		inline void solve(void)
		{
			sort(a+1,a+n+1); for (RI i=f[0]=1;i<=n;++i)
			if (!f[a[i]]) ++ans,expand(a[i]);
		}
		inline void clear(void)
		{
			for (RI i=1;i<=mx;++i) f[i]=0;
		}
}D;
int main()
{
	//freopen("money.in","r",stdin); freopen("money.out","w",stdout);
	for (F.read(t);t;--t)
	{
		RI i; for (F.read(n),mx=0,i=1;i<=n;++i) F.read(a[i]),mx=a[i]>mx?a[i]:mx;
		ans=0; D.solve(); printf("%d\n",ans); D.clear();
	}
	return 0;
}

赛道修建

很遗憾Day剩下的时间都耗在T3上了然而还是没想出来。暴力都写挂了

这题目看到一眼数据分治,具体的分治方法在游记里讲的也比较清楚了,自己去看下吧。

考场上我好像把最简单的的部分写挂了貌似忘记清空了,然后只有\(40pts\)

下面我们开始考虑正解,结合整个Day1的风格,这题还是要往贪心的方面想。

首先\(50000\)的范围提示\(n\log^2n\)做法,而且最大值最小显然是二分答案,这个先打着。

考虑从每个子树向上考虑答案,那么我们有一个比较显然的想法——类似于菊花图的做法把能合并的路径先合并了。

然后剩下的怎么办?选个最优的上传一下就好了啊,然后这题就做完了。

如果你对少爷机抱有自信的话直接用multiset当然是可以的,但是我比较菜,就写了排序+\(two\ points\)+二分的方法来匹配,当然速度就快了一些。

CODE

// luogu-judger-enable-o2
#include<cstdio>
#include<cctype>
#include<algorithm>
#define RI register int
#define add(x,y,z) e[++cnt]=(edge){y,head[x],z},head[x]=cnt
using namespace std;
const int N=50005;
struct edge
{
	int to,nxt,v;
}e[N<<1]; int head[N],a[N],f[N],n,m,cnt,x,y,z,sum,ans,ret;
class FileInputOutput
{
	private:
		#define S 1<<21
		#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
		char Fin[S],*A,*B;
	public:
		FileInputOutput() { A=B=Fin; }
		inline void read(int &x)
		{
			x=0; char ch; while (!isdigit(ch=tc()));
			while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
		}
		#undef S
		#undef tc
}F;
inline bool calc(int nc,int ndx,int x,int cnt)
{
	RI i,pos=1,cur=0; for (i=cnt;i>=pos;--i)
	{
		if (i==nc) continue; if (a[i]>=x) ++cur; else
		{
			while (pos<i&&a[pos]+a[i]<x) ++pos;
			if (pos==nc) ++pos; if (pos>=i) break; else ++cur,++pos;
		}
	}
	return cur==ndx;
}
#define to e[i].to
inline void check(int now,int fa,int x)
{
	RI i,cnt=0,pos=1,cur=0; for (i=head[now];i;i=e[i].nxt)
	if (to!=fa) check(to,now,x); for (i=head[now];i;i=e[i].nxt)
	if (to!=fa) a[++cnt]=f[to]+e[i].v; sort(a+1,a+cnt+1);
	for (i=cnt;i>=pos&&ret<m;--i) if (a[i]>=x) ++ret,++cur; else
	{
		while (pos<i&&a[pos]+a[i]<x) ++pos;
		if (pos>=i) break; else ++ret,++cur,++pos;
	}
	f[now]=0; int l=1,r=cnt,mid,res=-1; while (l<=r)
	if (calc(mid=l+r>>1,cur,x,cnt)) res=mid,l=mid+1; else r=mid-1;
	if (~res) f[now]=a[res];
}
#undef to
int main()
{
	//freopen("track.in","r",stdin); freopen("track.out","w",stdout);
	RI i; for (F.read(n),F.read(m),i=1;i<n;++i)
	{
		F.read(x); F.read(y); F.read(z);
		add(x,y,z); add(y,x,z); sum+=z;
	}
	int l=0,r=sum,mid; while (l<=r)
	if (ret=0,check(1,0,mid=l+r>>1),ret>=m) ans=mid,l=mid+1; else r=mid-1;
	return printf("%d",ans),0;
}

旅行

这题说实话是凭着欧气艹过去的,因此不想说太多。

首先你要发现这是一棵树/基环外向树,然后就可以很轻松的看出树的做法就是找最小的DFS序

这个直接用vector存边然后排个序跑一边就好了。冷静的10min写完

然后想基环树,这点基本套路我还是会的,所以马上想到枚举断边然后直接跑树的方法就好了。

找环?DFS就好了事实证明这里可能出了点问题,剩下的就是判字典序了我才不会告诉你我考场上手贱了判字典序写挂了两发

一共写了1h左右吧,CODE

#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#define RI register int
#define pb push_back
using namespace std;
const int N=5005;
vector <int> v[N]; int n,m,x,y;
class Tree_Solver
{
	private:
		int s[N],idx;
		inline void DFS(int now,int fa)
		{
			s[++idx]=now; int lim=v[now].size();
			for (RI i=0;i<lim;++i) if (v[now][i]!=fa) DFS(v[now][i],now);
		}
	public:
		inline void solve(void)
		{
			DFS(1,0); for (RI i=1;i<=n;++i) printf("%d%c",s[i],i^n?' ':'\n');
		}
}Case1;
class Circle_Tree_Solver
{
	private:
		int ans[N],s[N],idx,l[N],r[N],pre[N],L,R,cnt; bool vis[N],flag;
		inline void paint(int now)
		{
			while (pre[now]) l[++cnt]=pre[now],r[cnt]=now,now=pre[now];
		}
		inline void find(int now,int fa)
		{
			if (flag) return; vis[now]=1; int lim=v[now].size();
			for (RI i=0;i<lim;++i)
			{
				int to=v[now][i]; if (to==fa) continue;
				if (vis[to]) return (void)(l[++cnt]=fa,r[cnt]=now,flag=1,paint(fa));
				if (flag) return; pre[to]=now; find(to,now); if (flag) return;
			}
		}
		inline void DFS(int now,int fa)
		{
			s[++idx]=now; int lim=v[now].size();
			for (RI i=0;i<lim;++i)
			{
				int to=v[now][i]; if (to==fa) continue;
				if ((now==L&&to==R)||(now==R&&to==L)) continue; DFS(to,now);
			}
		}
		inline bool check(void)
		{
			if (!ans[1]) return 1; for (RI i=1;i<=n;++i)
			{
				if (s[i]>ans[i]) return 0;
				if (s[i]<ans[i]) return 1;
			}
			return 0;
		}
	public:
		inline void solve(void)
		{
			RI i; for (find(1,0),i=1;i<=cnt;++i)
			{
				L=l[i]; R=r[i]; idx=0;
				DFS(1,0); if (check()) memcpy(ans,s,sizeof(ans));
			}
			for (i=1;i<=n;++i) printf("%d%c",ans[i],i^n?' ':'\n');
		}
}Case2;
int main()
{
	//freopen("travel.in","r",stdin); freopen("travel.out","w",stdout);
	RI i; for (scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),v[x].pb(y),v[y].pb(x);
	for (i=1;i<=n;++i) sort(v[i].begin(),v[i].end()); return (m^n?Case1.solve():Case2.solve()),0;
}

填数游戏

从这一题开始难度瞬间飙升,考场上大力刚状压的尴尬就不想提了。

我就讲一下这题打表+找规律的做法吧。正解完全看不懂

首先我们要发现一个结论:每一斜行左下是连着的\(1\),右上是连着的\(0\)(当然可以全部\(0/1\)

这一性质就使得我们可以枚举每一条对角线的情况然后暴力搜索即可。

然后我们考虑这个条件的一个等价条件每个格子右边的格子先往下再往右的路径小于等于下面的格子先往右再往下的路径。

因为前者是往右走后最大的路径,后者是往下走后最小的路径,因此只要前者的字典序大于等于后者就是满足条件的方案。然后我们快速地写完爆搜+打表的代码:

#include<cstdio>
#define RI register int
const int N=105,mod=1e9+7;
using namespace std;
int a[N][N],n,m,ans;
inline bool calc(int x,int y)
{
    int rx=x,ry=y+1,dx=x+1,dy=y;
    while (rx+ry<=n+m)
    {
        if (a[rx][ry]!=a[dx][dy]) return a[rx][ry]<a[dx][dy];
        if (rx<n) ++rx; else ++ry; if (dy<m) ++dy; else ++dx;
    } return 1;
}
inline bool check(void)
{
    for (RI i=1;i<n;++i) for (RI j=1;j<m;++j)
    if (!calc(i,j)) return 0; return 1;
}
inline void DFS(int now)
{
    if (now>n+m) return (void)(ans+=check(),ans=ans^mod?ans:0);
    RI i; DFS(now+1); for (i=n;i;--i)
    if (now-i>=1&&now-i<=m) a[i][now-i]=1,DFS(now+1);
    for (i=n;i;--i) if (now-i>=1&&now-i<=m) a[i][now-i]=0;
}
int main()
{
    freopen("list_game.txt","w",stdout);
    for (RI i=1;i<=8;++i) for (RI j=1;j<=i+1;++j)
    ans=0,n=i,m=j,DFS(2),printf("ans[%d][%d]=%d,",n,m,ans);
    return 0;
}

然后你只需要耐心地等上一小会很久,就可以跑出\((8,8)\)内的所有答案和\((8,9)\)了。

然后根据这个我们结合那个广为人知的规律:\((n,m)=(n,m-1)\cdot3(m> n+1)\)就可以A掉了

tips:如果你再发现一下\((n,m)=(m,n)\)这个简单的性质以及\((n,n+1)=(n,n)\cdot3-3\cdot 2^n(n\ge4)\)的关系就可以更快的过了此题了。

CODE

#include<cstdio>
using namespace std;
const int N=10,mod=1e9+7;
int n,m,ans[N][N];
inline void make_list(void)
{
	ans[1][1]=2; ans[1][2]=4; ans[2][1]=4; ans[2][2]=12;
	ans[2][3]=36; ans[3][1]=8; ans[3][2]=36; ans[3][3]=112;
	ans[3][4]=336; ans[4][1]=16; ans[4][2]=108; ans[4][3]=336;
	ans[4][4]=912; ans[4][5]=2688; ans[5][1]=32; ans[5][2]=324;
	ans[5][3]=1008; ans[5][4]=2688; ans[5][5]=7136; ans[5][6]=21312;
	ans[6][1]=64; ans[6][2]=972; ans[6][3]=3024; ans[6][4]=8064;
	ans[6][5]=21312; ans[6][6]=56768; ans[6][7]=170112; ans[7][1]=128;
	ans[7][2]=2916; ans[7][3]=9072; ans[7][4]=24192; ans[7][5]=63936;
	ans[7][6]=170112; ans[7][7]=453504; ans[7][8]=1360128;
	ans[8][1]=256; ans[8][2]=8748; ans[8][3]=27216; ans[8][4]=72576;
	ans[8][5]=191808; ans[8][6]=510336; ans[8][7]=1360128; ans[8][8]=3626752; ans[8][9]=10879488;
}
inline int quick_pow(int x,int p,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{
	//freopen("game.in","r",stdin); freopen("game.out","w",stdout);
	scanf("%d%d",&n,&m); make_list();
    if (n==1) return printf("%d",quick_pow(2,m)),0;
    if (m==1) return printf("%d",quick_pow(2,n)),0;
    if (m-n<=1) return printf("%d",ans[n][m]),0;
	return printf("%d",1LL*ans[n][n+1]*quick_pow(3,m-n-1)%mod),0;
}

保卫王国

一眼不可做的神仙题系列,这个好像叫做那个动态DP什么的来着。。。

\(44pts\)的暴力就是一个经典的树形DP的板子,这里不再赘述。我们来讨论不超纲的做法——倍增+DP

首先我们给出几个状态数组:

  • \(in_{x,0/1}\):表示在\(x\)子树内\(x\)选(\(1\))或不选(\(0\))的最小代价
  • \(out_{x,0/1}\):表示在的子树外\(x\)选(\(1\))或不选(\(0\))的最小代价
  • \(anc_{x,i}\):表示\(x\)的向上\(2^i\)个节点所跳到的祖先,和LCA时的倍增数组类似。
  • \(f_{x,y,i,j}\):核心中的核心,表示在\(anc_{x,y}\)的子树与\(x\)的子树的差值部分中,\(x\)的状态为\(i\)(\(0/1\)),\(anc_{x,y}\)的状态为\(j(0/1)\)时的最小代价。这个我们在倍增的时候枚举一下转移的点的情况然后大力转移即可。

然后有了这些东西我们思考如何实现查询,我们首先用pair+set把无解的情况判了,然后:

  • 如果查询的点是祖先关系,那么直接把较深的点倍增到较浅的点上即可。中间的转移还是大力枚举状态。但是最后记得加上跳到的点的\(out\)的值
  • 否则我们把这两个点都跳到它们的LCA的儿子的位置,然后枚举LCA选或不选取最小值即可。

时间复杂度\(O(n\log n)\),还有一个枚举的常数(大约\(2^3\)左右?),很稳的跑了过去。

不得不说这题在思维难度和代码难度上都挺恐怖的。。。码了一个晚上

CODE

#include<cstdio>
#include<cctype>
#include<utility>
#include<set>
#define RI register int
#define mp make_pair
#define add(x,y) e[++cnt]=(edge){y,head[x]},head[x]=cnt
using namespace std;
typedef pair <int,int> pi;
typedef long long LL;
const int N=100005;
const LL INF=1e18;
struct edge
{
	int to,nxt;
}e[N<<1]; set <pi> s; int head[N],cnt,n,m,opt2,val[N],a,x,b,y; char opt1;
class FileInputOutput
{
	private:
		#define S 1<<21
		#define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
		#define pc(ch) (Ftop<S?Fout[Ftop++]=ch:(fwrite(Fout,1,S,stdout),Fout[(Ftop=0)++]=ch))
		char Fin[S],Fout[S],*A,*B; int Ftop,pt[25];
	public:
		inline void gc(char &ch)
		{
			while (!isalpha(ch=tc()));
		}
		inline void read(int &x)
		{
			x=0; char ch; while (!isdigit(ch=tc()));
			while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
		}
		inline void write(LL x)
		{
			if (!x) return (void)(pc('0'),pc('\n')); if (x<0) pc('-'),x=-x; RI ptop=0;
			while (x) pt[++ptop]=x%10,x/=10; while (ptop) pc(pt[ptop--]+48); pc('\n');
		}
		inline void Fend(void)
		{
			fwrite(Fout,1,Ftop,stdout);
		}
		#undef S
		#undef tc
		#undef pc
}F;
class Double_Increased_On_Tree
{
	private:
		#define P 18
		LL in[N][2],out[N][2],f[N][P][2][2]; int anc[N][P],dep[N]; //f[i][j][k][p] status(i)=k,status(anc[i][j]=p)
		inline LL min(LL a,LL b)
		{
			return a<b?a:b;
		}
		inline void swap(int &x,int &y)
		{
			int t=x; x=y; y=t;
		}
	public:
		#define to e[i].to
		inline void DFS1(int now,int fa)
		{
			in[now][1]=val[now]; dep[now]=dep[fa]+1;
			for (RI i=head[now];i;i=e[i].nxt) if (to!=fa)
			{
				anc[to][0]=now; DFS1(to,now);
				in[now][0]+=in[to][1]; in[now][1]+=min(in[to][0],in[to][1]);
			}
		}
		inline void DFS2(int now)
		{
			for (RI i=head[now];i;i=e[i].nxt) if (to!=anc[now][0])
			{
				out[to][0]=out[now][1]+in[now][1]-min(in[to][0],in[to][1]);
				out[to][1]=min(out[now][0]+in[now][0]-in[to][1],out[to][0]); DFS2(to);
			}
		}
		#undef to
		inline void init(void)
		{
			RI i; for (i=1;i<=n;++i)
			{
				int fa=anc[i][0]; f[i][0][0][0]=INF; f[i][0][1][0]=in[fa][0]-in[i][1];
				f[i][0][0][1]=f[i][0][1][1]=in[fa][1]-min(in[i][0],in[i][1]);
			}
			for (RI j=1;j<P;++j) for (RI i=1;i<=n;++i)
			{
				int son=anc[i][j-1]; anc[i][j]=anc[son][j-1];
				for (RI u=0;u<=1;++u) for (RI v=0,w;v<=1;++v)
				{
					for (f[i][j][u][v]=INF,w=0;w<=1;++w)
					f[i][j][u][v]=min(f[i][j][u][v],f[i][j-1][u][w]+f[son][j-1][w][v]);
				}
			}
		}
		inline LL query(int x,int a,int y,int b) //like to swap(x,a),swap(y,b)
		{
			if (dep[x]<dep[y]) swap(x,y),swap(a,b); LL mx[2]={INF,INF},my[2]={INF,INF},tx[2],ty[2];
			RI i,u,v; for (mx[a]=in[x][a],my[b]=in[y][b],i=P-1;~i;--i) if (dep[anc[x][i]]>=dep[y])
			{
				for (tx[0]=tx[1]=INF,u=0;u<=1;++u) for (v=0;v<=1;++v)
				tx[u]=min(tx[u],mx[v]+f[x][i][v][u]); mx[0]=tx[0]; mx[1]=tx[1]; x=anc[x][i];
			}
			if (x==y) return mx[b]+out[x][b]; for (i=P-1;~i;--i) if (anc[x][i]!=anc[y][i])
			{
				for (tx[0]=tx[1]=ty[0]=ty[1]=INF,u=0;u<=1;++u) for (v=0;v<=1;++v)
				tx[u]=min(tx[u],mx[v]+f[x][i][v][u]),ty[u]=min(ty[u],my[v]+f[y][i][v][u]);
				mx[0]=tx[0]; mx[1]=tx[1]; my[0]=ty[0]; my[1]=ty[1]; x=anc[x][i]; y=anc[y][i];
			}
			int fa=anc[x][0]; return min(in[fa][0]-in[x][1]-in[y][1]+mx[1]+my[1]+out[fa][0],
			in[fa][1]-min(in[x][0],in[x][1])-min(in[y][0],in[y][1])+min(mx[0],mx[1])+min(my[0],my[1])+out[fa][1]);
		}
}T;
int main()
{
	//freopen("defense.in","r",stdin); freopen("defense.out","w",stdout);
	RI i; for (F.read(n),F.read(m),F.gc(opt1),F.read(opt2),i=1;i<=n;++i) F.read(val[i]);
	for (i=1;i<n;++i) F.read(x),F.read(y),add(x,y),add(y,x),s.insert(mp(x,y));
	for (T.DFS1(1,0),T.DFS2(1),T.init(),i=1;i<=m;++i)
	{
		F.read(a); F.read(x); F.read(b); F.read(y); if (!x&&!y)
		{
			pi A=mp(a,b),B=mp(b,a); if (s.find(A)!=s.end()||s.find(B)!=s.end()) { F.write(-1); continue; }
		}
		F.write(T.query(a,x,b,y));
	}
	return F.Fend(),0;
}

Postscript

感觉今年的题目风格及其诡异,Day1从温暖Day2劝退。

还是自己菜,难一点的题目就切出不来,最后落到个写写暴力划水苟个一等的下场。

无缘冬令营了,希望\(ZJOI2019\)这场初中的最后一场比赛可以不被吊打吧,接下来的这一年要更加拼命了。

posted @ 2018-11-23 21:33  空気力学の詩  阅读(299)  评论(2编辑  收藏  举报