NOI 2016 题目选做

网格

题目描述

点此看题

解法

首先有一个关键的 \(\tt observation\):答案不会超过 \(2\)(可以直接封锁边界点),那么根据众多 \(\tt CF\) 题目的经验,我们可以直接开始分类讨论:

  • 如果只剩一个跳蚤,或者只剩两个跳蚤并且它们联通,那么答案是 -1
  • 如果已经存在两个跳蚤不连通,那么答案是 0
  • 把相邻的跳蚤连边,如果原图存在割点那么答案是 1
  • 否则答案是 2

那么暴力建图可以做到 \(O(nm)\) 的复杂度,一个比较显然的思路是保留每个障碍旁边的若干点,虽然貌似这种做法可以通过官方数据但是很容易被 \(\tt hack\),这里介绍一种不需要特判的优化建图方法,首先我们保留这些点:

  • 距离四个角 \(2\times 2\) 范围内的点。
  • 和某个障碍物八联通的点。
  • 和某个障碍物在同一行\(/\)列,并且自己身处边界的点。

那么对于保留的点,如果两个点在同一行或者同一列并且中间没有障碍物,就连一条边。最后暴力 \(\tt tarjan\) 求割点即可,时间复杂度 \(O(c)\)

正确性证明待补充。

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 10000005;
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,c,q,t,cnt,zxy,dfn[M],low[M];
vector<int> g[M];
struct node {int x,y,t;}a[M];
bool cmp1(node a,node b)
{
	if(a.x==b.x && a.y==b.y) return a.t<b.t;
	if(a.x==b.x) return a.y<b.y;
	return a.x<b.x;
}
bool cmp2(node a,node b)
{
	return a.y==b.y?a.x<b.x:a.y<b.y;
}
void add(int x,int y,int dx,int dy)
{
	for(int i=-dx;i<=dx;i++)
		for(int j=-dy;j<=dy;j++)
		{
			int tx=x+i,ty=y+j;
			if(tx>=1 && tx<=n && ty>=1 && ty<=m)
				a[++t]={tx,ty,0};
		}
}
void dfs(int u,int fa)
{
	dfn[u]=low[u]=++cnt;int son=0;
	for(int v:g[u])
	{
		if(!dfn[v])
		{
			dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				if(fa) zxy=1;
				else son++;
			}
		}
		else if(v!=fa) low[u]=min(low[u],dfn[v]); 
	}
	if(son>1) zxy=1;
}
void work()
{
	n=read();m=read();c=read();q=t=0;
	for(int i=1;i<=c;i++)
	{
		int x=read(),y=read();
		add(x,y,1,1);
		add(1,y,0,0);add(n,y,0,0);
		add(x,1,0,0);add(x,m,0,0);
		a[++t]={x,y,-1};
	}
	add(1,1,2,2);add(1,m,2,2);add(n,1,2,2);add(n,m,2,2);
	sort(a+1,a+1+t,cmp1);int pt=0;node ls;
	for(int i=1;i<=t;i++)
		if(ls.x!=a[i].x || ls.y!=a[i].y)
			ls=a[i],a[++pt]=ls;
	t=pt;cnt=zxy=0;
	for(int i=1;i<=t;i++)
		if(a[i].t!=-1) a[i].t=++q;
	for(int i=1;i<=q;i++)
		dfn[i]=low[i]=0,g[i].clear();
	for(int i=2;i<=t;i++)
		if(a[i].x==a[i-1].x && a[i].t!=-1 && a[i-1].t!=-1)
			g[a[i].t].pb(a[i-1].t),g[a[i-1].t].pb(a[i].t);
	sort(a+1,a+1+t,cmp2);
	for(int i=2;i<=t;i++)
		if(a[i].y==a[i-1].y && a[i].t!=-1 && a[i-1].t!=-1)
			g[a[i].t].pb(a[i-1].t),g[a[i-1].t].pb(a[i].t);
	if(q<=1 || (q<=2 && !g[1].empty()))
	{
		puts("-1");
		return ;
	}
	dfs(1,0);
	if(cnt<q) puts("0");
	else puts(zxy?"1":"2");
}
signed main()
{
	T=read();
	while(T--) work();
}

国王饮水记

题目描述

点此看题

解法

没有什么好的思路,但是本题性质应该是比较多的,来做一些基本的观察:

  • 每个非 \(1\) 城市只能使用一次,因为使用一次之后水位不比 \(1\) 大,而且不会被拿来当跳板。
  • 水位比 \(1\) 低的城市一定没用,可以在一开始就把它们删除。
  • 如果次数足够多,那么一定是把 \(h\) 从小到大排序后依次和 \(1\) 两两操作。

根据上面的观察,可以拓展到次数平凡的情况。考虑最优策略一定是:舍弃掉开头的一段,把后面的城市分成连续的 \(k\) 段,然后依次操作每一段,并且满足段长单调不增。

显然可以用 \(dp\) 来规划划段的过程,设 \(f[i][j]\) 表示划分到 \(i\),已经划分了 \(j\) 段,现在最大的 \(h_1\) 是多少。那么直接枚举上一次划段的转移点即可:

\[f[i][j]\leftarrow (f[k][j-1]+\sum_{l=k+1}^i h_l)/(i-k+1) \]

暴力转移时间复杂度 \(O(n^3p)\),优化可以考虑把转移写成前缀和的形式,设 \(s_i=\sum_{j=1}^i h_j\)

\[f[i][j]\leftarrow (f[k][j-1]+s_i-s_k)/(i-k+1) \]

\[f[i][j]\leftarrow (s_i-(s_k-f[k][j-1]))/(i-(k-1)) \]

然后真的就是一个不是很典型的斜率优化了,考虑把转移的含义看成最大化斜率。具体来说就是,平面上有 \((i-1,s_i-f[i][j-1])\) 的转移点,要求 \((i,s_i)\) 到选定转移点连成直线的斜率最大。

画一下图就知道要维护转移点的下凸包(斜率单增),然后在凸包上三分就可以做到 \(O(n^2\log n\cdot p)\),分析代价可知最优转移点单调不降,那么就可以维护队列,不优时直接弹出头部即可,时间复杂度 \(O(n^2p)\)

要获得最后的 \(15\) 分我们需要一个神奇性质,那就是非 \(1\) 的段只有 \(O(\log n)\) 个,那么我们只需要 \(dp\) 大概 \(14\) 层即可,后面的都拿单个补齐。由于层数很少 \(dp\) 的时候可以直接用 \(\tt double\),得到转移点之后再用高精度计算即可。

时间复杂度 \(O(n\log n+np)\),注意出题人给的高精板子 PREC 应该改成 \(6000\)(其实这是高精度的位数,但是初始设置的是 \(2100\),对于最后一个点不够用),我给出的代码中去掉了这个高精板子。

但是最后的神奇性质我仍然不会证明,也许可以通过打表转移点观察出来?

#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 8005;
#define db double
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
//

//
int n,m,w,a[M],h[M],s[M],g[M][20],p[M];db f[M][20];
struct node{db x,y;}q[M];
db slope(node a,node b)
{
	return (a.y-b.y)/(a.x-b.x);
}
Decimal calc(int i,int j)
{
	if(!j) return h[1];
	return (calc(g[i][j],j-1)+s[i]-s[g[i][j]])/(i-g[i][j]+1);
}
signed main()
{
	n=read();m=read();w=read();
	h[1]=read();int k=1;
	for(int i=2;i<=n;i++)
	{
		int x=read();
		if(x>=h[1]) h[++k]=x;
	}
	n=k;m=min(m,n);k=min(m,14);sort(h+1,h+1+n);
	for(int i=1;i<=n;i++)
		f[i][0]=h[1],s[i]=s[i-1]+h[i];
	for(int j=1;j<=k;j++)
	{
		int l=1,r=1;p[1]=1;
		for(int i=1;i<=n;i++)
			q[i]=node{i-1,s[i]-f[i][j-1]};
		for(int i=2;i<=n;i++)
		{
			node u={i,s[i]};
			while(l<r && slope(u,q[p[l]])
				<slope(u,q[p[l+1]])) l++;
			g[i][j]=p[l];
			f[i][j]=(f[p[l]][j-1]+s[i]-s[p[l]])/(i-p[l]+1);
			while(l<r && slope(q[p[r-1]],q[p[r]])
				>slope(q[p[r]],q[i])) r--;
			p[++r]=i;
		}
	}
	int o=n-m+k,u=0;
	for(int i=0;i<=k;i++)
		if(f[o][i]>f[o][u]) u=i;
	Decimal ans=calc(o,u);
	for(int i=o+1;i<=n;i++)
		ans=(ans+h[i])/2;
	cout<<ans.to_string(w<<1)<<endl;
}
posted @ 2022-04-30 11:19  C202044zxy  阅读(189)  评论(0编辑  收藏  举报