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\) 是多少。那么直接枚举上一次划段的转移点即可:
暴力转移时间复杂度 \(O(n^3p)\),优化可以考虑把转移写成前缀和的形式,设 \(s_i=\sum_{j=1}^i h_j\):
然后真的就是一个不是很典型的斜率优化了,考虑把转移的含义看成最大化斜率。具体来说就是,平面上有 \((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;
}