您的位置:首页 > 其它

NOIP2015复赛提高组day2(A:跳石头 B:子串 C:运输计划)

2016-11-14 16:58 281 查看
A题:

水题,

二分答案,然后每次跳大于这个数值的最小距离就行了

如果最后一步距离不够,

那么前一步直接跳到终点

(博主并不知道这怎么证明)

只是这种方法一想到就觉得是显然的

但是博主还是有点慌

于是倒过来又跳了一遍

然而其实不用倒过来也能A,于是给出的代码不会有倒过来跳的部分Orz。。。。。

#include<bits/stdc++.h>
using namespace std;
#define M 50005
void Rd(int &res){
char c;res=0;
while(c=getchar(),!isdigit(c));
do res=(res<<3)+(res<<1)+(c^48);
while(c=getchar(),isdigit(c));
}
int a[M];
int len,n,m;
bool check(int x){//check
int step=0,pre=1,res=0;
for(int i=1;i<=n+1;i++){
step+=a[i];
if(step<x)continue;
res+=i-pre;
step=0;
pre=i+1;
}
if(step&&step<x){
res+=n+2-pre;
}
return res<=m;
}
int main(){
scanf("%d %d %d",&len,&n,&m);
for(int i=1;i<=n;i++)Rd(a[i]);
a[n+1]=len-a
;
for(int i=n;i>=1;i--)a[i]-=a[i-1];
int l=0,r=len,res=-1;
while(l<=r){//二分
int mid=l+r>>1;
if(check(mid)){
res=mid;
l=mid+1;
}
else r=mid-1;
}
printf("%d\n",res);
return 0;
}


B题:

这其实是一道很水的dp题

xiaoC大人说这已经是联赛最难的dp题了。。。。。

什么鬼??

那我写那么多单调队列、单调栈优化的dp有什么意义!!

好吧,作为1个程序员,

知晓各种解题方法还是很有必要的

这题很明显是一个三维的dp问题

对于字符串a的第i个字符

字符串b的第j个字符前面已经匹配了p个子串的情况

我们当前的ai如果和bj相等

就可以选择选或不选当前字符

而如果前面的ai−1和是不选的,那么子串个数+1

如果是选的,那么子串个数有+1和不变两种情况。。

(别问我这是为什么,这题目有点神奇。。。)

然而这会炸内存

于是乎我们发现每一个点的状态只会从它的上一个点转移过来

于是就可以用滚动数组优化空间了

于是

时间复杂度:O(n⋅m⋅k),空间复杂度O(m⋅k)

一道水题就被我们完美的解决了。。。

get代码:

#include<stdio.h>
#define M 1005
#define N 205
const int P=1e9+7;
int dp[2]

[2];
char a[M],b
;
int main(){
int n,m,k;
scanf("%d %d %d",&n,&m,&k);
scanf("%s",a+1);
scanf("%s",b+1);
dp[0][0][0][0]=1;
int cur=0;
for(int i=1;i<=n;i++){
cur=!cur;
for(int j=0;j<=m;j++)
for(int p=0;p<=k&&p<=j;p++){
dp[cur][j][p][0]=(dp[!cur][j][p][0]+dp[!cur][j][p][1])%P;//不相同,不选
if(p&&a[i]==b[j])//相同
dp[cur][j][p][1]=(1ll*dp[!cur][j-1][p-1][0]+dp[!cur][j-1][p][1]+dp[!cur][j-1][p-1][1])%P;//选+不选
else dp[cur][j][p][1]=0;//选0个子串或两字符不同,不能选。。
}
}
int ans=0;
printf("%d\n",(dp[cur][m][k][1]+dp[cur][m][k][0])%P);
return 0;
}


C题:

这题是一道好题

典型的卡常数题目

如果你是树链剖分+线段树的话,毫无疑问会被卡掉5分

不过这种无脑方法却是最容易想到,且性价比最高的,

借用xiaoC大人的话来说就是:

联赛第三题你都95分了还想怎样?

确实够了

与其去纠结这5分

还不如去对拍前两题

万一前两题WA了,得不偿失啊

会血崩的

但是这种无脑解法我是不会去敲的。。

这里给出两种解法(AC)

我们先讲诉第一种:

树链剖分+LCA+二分+差分前缀和

我们在刚看到这题的时候,可以从题意很明显的看出这是一个最大值最小问题

那么很容易就想到了二分

而因为这题明显要求树上两点间距离

所以这是要先求LCA

(树上任意两点之间距离等于他们到根节点的距离之和减去他们的LCA到达根节点的距离的两倍)

若设disi为结点i到根节点的距离

则len:x−>y=disx+disy−2∗dislca(x,y)

求LCA最快的方法是什么?

当然是树链剖分

好吧,这里先贴一个树链剖分求LCA的方法

树链剖分是什么呢

说白了就是

把一棵树分成一条又一条的链

而后在这些链上我们就可以用线段树,树状数组等各种数据结构优化

还可以很轻松的求出LCA

但你如果直接乱抽链肯定是不行的

有时候会导致你造出的链太多而导致复杂度退化

而事实上我们要保证树链剖分最多分出log2n条链

这样才能保证复杂度

那么怎么抽链呢

这个算法的发明人是这样做的:

我们先解释一下重儿子这个概念:

重儿子解释结点x的子孙最多的儿子

还有重链:顾名思义,重链就是从某一点开始,一直往下找重儿子所组成的链

那么我们抽链的时候就可以一直抽重链

对于某一个结点x而言

我们往他的重儿子方向递归的时候将其加入结点x所在的重链

往非重儿子方向递归的时候新建一条链

于是我们完美的造出了重链

当然,

对于每一条重链五门都需要存储一下这条链的顶端的结点编号

别问我为什么,树链剖分就是这样的,唯一的解释是,这样快。。

而后求LCA的时候,我们只需要

每次跳深度大的点,

将其跳到改点所在重链的顶端点的父亲结点

当这两个点在同一条重链上时

深度小的就是LCA

仔细思考一下为什么,蒟蒻就不解释了

代码就不附加了

C题的代码里包括了。。。。

(不想复制粘贴,累)

复杂度为O(log2n)

倍增虽然有一样的复杂度都比这慢多了

由于这题会卡常数,

还是用树链剖分比较好。。

还有存边千万不能用vector,正向表(链表)才是王道

求出LCA以后,

我们在二分的时候每一次check

都需要计算出所有在

ti【二分得出】时间内走不到终点的路径的最大公共边

而后判断长度最大的路径减去这个公共边的长度是否小于ti

那么怎么找出这条路径呢,

我们知道直接用路径进行操作无疑是很麻烦的

于是我们要把路径的信息压到点上

于是这个时候仔细想想就会发现

对于树上任意一个非根节点而言

都有唯一一条通向他的父亲的边

因为这是一一对应的

所以我们可以把这条边的信息转移到这个点上来操作

这个时候我们只需要使用一个差分前缀和

使每条路径两个端点的差分值+1,LCA的差分值−2

最后向下dfs的时候把儿子所对应边的覆盖次数加上该点的差分值

就是这个结点所对应的边被覆盖的次数

(虽说是用结点来处理,但其实本质上还是边,只是用结点的编号来代替边)

然而这样的系数是很高的

我们首先要用树链剖分求LCA

建树等等的相加这大约需要40∗300000=12000000的时间

sort需要的时间大约是8∗300000=2400000

最大的路径长度为3e8,于是二分的次数大约为log2(1e9)=28次

而每一次二分,

我们都要用O(m)的时间计算有多少条路径在ti时间内走不到终点

而后用O(n)的时间dfs求出每条边被覆盖的次数

最后还要用O(n)的时间选择最长公共边

于是二分+check的部分需要

28∗(m+n+n)

然而系数是很大的(我觉得至少加起来有10几【三个变量m、n、n相加】)

所以这个阶段的时间大约是28∗300000∗13

所以总时间大约是123600000

好吧,我发现我似乎算小了

其实怎么说这样写也是基本要破2000ms的

但是我们怎么进行优化呢

我们发现二分的次数太多了

但仔细一看题目我们就会灵光一现

每条边的长度不超过1000)

这个就厉害了

我们知道,

删除的边必然在最长的那条路径上(设其长度为len),

并且只能删一条边

那么最终答案只会在区间【len−1000,len】内!!!

这下我们二分的次数降到了10次

这下基本就可以A了

就算还是A不掉

加些lnline、register之类的黑科技就好了

还是过不了的话仔细阅览代码,

把能去掉或合并的步骤尽量去除、合并

比如说O(n)复杂度选择选择最长公共边的步骤完全可以在rdfs上直接运行(大约块30ms)

还有每个点的被覆盖次数完全可以直接用差分数组存储(这样会快100ms,我也不知道为什么这么多)

(或者你可以把if进行可能性大的放在前面,把if(y==pre)continue;之类的语句改成if(y!=pre)都会变快)博主这样敲完了在慢的1B的内部网站上939ms(和noip的老爷机测评速度差不多)

在codevs上应该有850ms

代码:

//PS:树链剖分+LCA+二分+差分前缀和
#include<bits/stdc++.h>
using namespace std;
#define M 300001
inline void Rd(int &res){
char c;res=0;
while(c=getchar(),!isdigit(c));
do res=(res<<3)+(res<<1)+(c^48);
while(c=getchar(),isdigit(c));
}
int tot;
int head[M],dep[M],fa[M],sz[M],son[M],dis[M],top[M];
//head[x]->正向表专用,用于记录表头(表头无值时为-1)
//dep[x]表示结点x在树上的深度(以1为根)
//fa[x]代表结点x的父亲结点
//sz[x]代表以结点x为根的子树的大小
//son[x]代表结点x的重儿子
//dis[x]代表结点x到达根节点的距离
//top[x]代表结点x所在重链深度最小的点
int n,m;
struct LI{//正向表
int to,v,nxt;
}edge[M<<1];
struct node{
int s,t,lca,len;
bool operator <(const node &A)const{
return len>A.len;
}
}A[M];
//A[x]代表编号为x的路径,len代表其长度,s、t、lca分别为两端点及其最近公共祖先
//PS:黑科技‘inline’只能在没调用其他任何函数的函数上使用
//register我不知道有什么限制,反正博主只把他用在循环变量上
inline void Add(int a,int b,int c){//加边
edge[tot].to=b;edge[tot].v=c;edge[tot].nxt=head[a];head[a]=tot++;
}
void dfs(int x,int pre,int d,int len){
dep[x]=d;
dis[x]=len;
sz[x]=1;
int mx=0;
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pre)continue;
dfs(y,x,d+1,len+edge[i].v);
sz[x]+=sz[y];
if(sz[y]>mx){
mx=sz[y];
son[x]=y;//求重儿子
}
fa[y]=x;
}
}
int mx;
void Rdfs(int x,int f,int pre){//造重链
top[x]=pre;
if(son[x])Rdfs(son[x],x,pre);
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y!=f&&y!=son[x])Rdfs(y,x,y);
}
}
inline int LCA(int a,int b){//求LCA
while(top[a]!=top){
if(dep[top[a]]<dep[top[b]])swap(a,b);
a=fa[top[a]];
}
if(dep[a]<dep[b])return a;
return b;
}
int add[M];
//add[x]表示在rdfs前表示每个点的差分值,而在rdfs后表示每个节点所对应的边被覆盖的次数
int k;
/*
如果使用cnt数组存储每个节点所对应的边被覆盖的次数的话
下面的rdfs函数应该是这样的
void rdfs(int x,int pre){
cnt[x]=add[x];
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pre)continue;
rdfs(y,x);
cnt[x]+=add[y];
}
if(cnt[x]==k)
if(dis[x]-dis[pre]>mx)mx=dis[x]-dis[pre];
}
*/
void rdfs(int x,int pre){//求每一条边被覆盖的次数
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pre)continue;
rdfs(y,x);
add[x]+=add[y];//计算点x所对应的边被覆盖的次数
}
if(add[x]==k)//判断改点所对应的边是否被所有不可行路径覆盖
if(dis[x]-dis[pre]>mx)mx=dis[x]-dis[pre];//计算最长公共边的长度
}
inline bool check(int ti){
k=m;
memset(add,0,sizeof(add));
for(register int i=1;i<=m;++i){
if(A[i].len<=ti){//求出不可行路径条数
k=i-1;
break;
}
++add[A[i].s];//累加差分前缀和
++add[A[i].t];
add[A[i].lca]-=2;
}
mx=0;
rdfs(1,0);//计算所有不可行路径的最长公共边
return A[1].len-mx<=ti;
}
int main(){
int size=32<<20;//32M
char *p=(char*)malloc(size)+size;//栈外挂,noip不需要
__asm__("movl %0, %%esp\n"::"r"(p));
scanf("%d %d",&n,&m);
memset(head,-1,sizeof(head));
for(register int i=1;i<n;++i){
int a,b,c;
Rd(a);Rd(b);Rd(c);
Add(a,b,c);//加边
Add(b,a,c);
}
dfs(1,0,1,0);//造树
Rdfs(1,0,1);//造重链,树链剖分
for(register int i=1;i<=m;++i){
Rd(A[i].s);Rd(A[i].t);
A[i].lca=LCA(A[i].s,A[i].t);//求LCA
A[i].len=dis[A[i].s]+dis[A[i].t]-(dis[A[i].lca]<<1);//改路径长度
}
sort(A+1,A+m+1);
int l=max(A[1].len-1000,0),r=A[1].len,res;
while(l<=r){//二分答案
int mid=l+r>>1;
if(check(mid)){//判断是否可行
res=mid;
r=mid-1;
}
else l=mid+1;
}
printf("%d\n",res);
return 0;
}


然而这个方法考试的时候是很难A掉了

毕竟你不知道自己的代码究竟会不会被常数这个恶心的东西卡掉

 

  

 

  

  

下面介绍liweijun大神的神奇方法

[b]我也不知道这算什么方法


不知道大神是怎么y出这种神奇方法的

(去了树链剖分求LCA,其他部分都是O(n)的复杂度)

我们考虑到删除的边必然在最长的路径上

于是我们可以吧这棵树换成这一模样:

一条链,链上的每个节点挂着一些子树,如下图:

那么我们也能知道删除这条边后的最大路径长度

要么该路径长度-所删边长度

要么是不经过这条边的最长路径

于是我们可以预处理出某一点右端的最长路径(后缀)

和该点左端的最长路径长度(前缀)

这个怎么预处理呢?



我们来考虑这么一条路径吧

(你应该能看出来哪一条是路径)

我们知道这条路径在最长路径上有左右端点

那么我们明显知道这条路径可以更新左端点的前缀和右端点的后缀

但这时更新的只是单点前缀和后缀

最后一遍for过来把整条链的前缀后缀更新一下就好了

当然,我们需要用dfs求出链上的每一点的子树上有哪些点

及求出每个点所对应的链上的点是哪个

最后枚举删去那条边,

取他的前缀、后缀以及最长路径长度减去该边长度的最大值就行了

这种方法只有400ms,非常快

代码:

//由于和上一种方法有很大一部分是一样的,所以只有核心代码
int mark[M];
//mark[x]==1代表点x在最长路径上
int mx1[M],mx2[M];
//mx1[x]代表点x的前缀,mx2代表点x的后缀
inline void LCA2(int a,int b){//找出最长链上的所有点
if(dep[a]<dep[b])swap(a,b);
while(dep[a]>dep[b]){
mark[a]=1;
a=fa[a];
}
while(a!=b){
mark[a]=1;
a=fa[a];
mark[b]=1;
b=fa[b];
}
mark[a]=1;
}
int la[M];
//la[x]代表结点x所对应的链上的点在树上的编号
int num;
int id[M],step[M];
//id[x]代表链上的点x在原树上的编号
//step[x]代表在链上的点x到下一点的距离
void edfs(int x,int pre){//将最长路上的点连接成链
id[++num]=x;
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pre||!mark[y])continue;
step[num]=edge[i].v;
edfs(y,x);
}
}
void fdfs(int x,int pre,int f){//找出每个节点在链上的对应点
la[x]=f;
for(register int i=head[x];~i;i=edge[i].nxt){
int y=edge[i].to;
if(y==pre||mark[y])continue;
fdfs(y,x,f);
}
}
int ans=-1;
void solve(){
int k,mx=-1;
for(register int i=1;i<=m;++i)
if(A[i].len>mx){//找最长路径
mx=A[i].len;
k=i;
}
LCA2(A[k].s,A[k].t);//将最长路径上的点全部mark
edfs(A[k].s,0);//将最长路径抽成一条链
for(register int i=1;i<=num;++i)fdfs(id[i],0,i);//找出每一个点对应的链上点
for(register int i=1;i<=m;++i){//预处理链上每一点的前缀和后缀
if(i==k)continue;
int L=la[A[i].s],R=la[A[i].t];
if(L>R)swap(L,R);
if(A[i].len>mx2[L])mx2[L]=A[i].len;
if(A[i].len>mx1[R])mx1[R]=A[i].len;
}
for(register int i=2;i<=num;++i){//预处理链上每一点的前缀和后缀
if(mx1[i-1]>mx1[i])mx1[i]=mx1[i-1];
if(mx2[num-i+2]>mx2[num-i+1])mx2[num-i+1]=mx2[num-i+2];
}
for(register int i=1;i<num;++i){//枚举删除那一条边
int res=max(A[k].len-step[i],max(mx1[i],mx2[i+1]));
if(res<ans||ans==-1)ans=res;
}
if(ans!=-1)printf("%d\n",ans);
else puts("0");
}


今天265,马马虎虎

可是昨天炸了

加起来只有445
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: