TEST2018.5.12(Noip2016提高组Day1)

TEST2018.5.12(Noip2016提高组Day1)

为2019级lizw0520zzh的学生献上福利!!!

题目描述(感谢洛谷爸爸):    玩具谜题    天天爱跑步    换教室


T1玩具谜题:

简单到不能再简单的模拟题了,思路就难得写了(因为后面两道题有点小难)

代码如下(第一次养成写注释的习惯):

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=100000+233;
struct Character {
	int face;
	char S[15];
}A[N];
int n,m;
int start=1;

int main() {
	freopen("toy.in","r",stdin);
	freopen("toy.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d %s",&A[i].face,&A[i].S);    //0朝向圈内,1朝向圈外
	for(int a,s,i=1;i<=m;i++) {
		scanf("%d%d",&a,&s);    //0左数,1右数
		if(a==0)
			if(A[start].face==0) start+=n,start-=s,start%=n;
			else if(A[start].face==1) start+=s,start%=n;
		if(a==1)
			if(A[start].face==0) start+=s,start%=n;
			else if(A[start].face==1) start+=n,start-=s,start%=n;
		if(start==0) start+=n;
	}
	printf("%s\n",A[start].S);
	return 0;
}


T2天天爱跑步:

这道题,嗯,很难。。。

直接说思路吧:

首先解决本题应用的知识点:

dfs序——将求子树的信息(树形)转化为求一段连续区间信息(线形)

线段树——求区间信息

树上差分——统计答案

lca——拆分路径

树链剖分——求lca

 

另deep[]表示节点的深度,watch[]表示观察者的出现时间,s表示玩家起点,t表示终点

固定节点的观察者出现的时间固定,说明对这个观察者有贡献的点是有限且固定的

只有满足  观察者出现时间=玩家起点与观察者距离的  玩家才对观察者有贡献

 

每条路径拆成   起点到lca(向上跑)  和   终点到lca的子节点(向下跑)  的两条路径

对于向上跑的,如果玩家能被观察员i观察到,那么deep[s]-deep[i]=watch[i]   式①

对于向下跑的,就是 deep[s]+deep[i]-2*deep[lca(s,i)]=watch[i]  式②

等号左边是玩家起点与观察者的距离,等号右边是观察者出现时间

向上跑的很显然,向下跑的如何理解?

假设我们知道点a,b到lca(a,b)的距离分别为da,db,那么a,b之间的距离=da+db

但这里的deep不是到lca的距离,是深度,即到根节点的距离+1

deep[s]+deep[i]包含2段信息,1、s到i的距离,   2、lca(s,i)到根节点的距离+1 

第2段包含了2次,所以减去

 

先看向上跑的

玩家路径:玩家起点 到 起点与终点的lca

将式①移项,deep[s]=deep[i]+watch[i]

发现等号右边是定值

也就是说对与观察者i,他所能观察到的向上跑的玩家,是所有的起点=deep[i]+watch[i]的玩家

换句话说,以i为根的子树中,所有深度为deep[i]+watch[i]的玩家都能被i观察到

我们如果搞一个dfs序,i的在a时入栈,在b时出栈,

那么以i为根的子树就可以转化为区间[a,b]

深度咋整?

我们对每个深度建立一颗线段树(动态加点)

那么问题就转化为了  在深度为deep[i]+watch[i]的线段树中,查询区间[a,b]的玩家个数

现在就差玩家个数了

很容易想到在起点处+1

但是还要在起点与终点的lca的父节点处-1

差分惯用思想

用sum[]统计这些1和-1的和

那么问题就转化为了  在深度为deep[i]+watch[i]的线段树中,查询区间[a,b]的sum和

 

提问:为什么是起点处+1,lca的父节点处-1,可以反过来吗?

不可以。

因为起点的深度深,lca的父节点深度浅,在深度深的节点处+1,以深度深度浅的点为根的子树可以包含这个点

想想序列上的差分,是左端点+1,右端点后面的点-1

因为序列差分与前缀和相联系,前面的点的信息对后面的点会产生影响,所以只需加一个1

这里查询的是子树信息,是这个点深度及以下的信息

对照理解即可

 

向下跑的同理,只简单说怎么做

玩家路径:lca的子节点到玩家终点

把式②移项 deep[s]-2*deep[lca(s,i)]=watch[i]-deep[i]

在watch[i]-deep[i]深度为deep[s]-2*deep[lca(s,i)]的线段树中,终点处+1,lca处-1

查询时查深度为watch[i]-deep[i]的线段树即可

 

2个小问题:

1、做完向上跑的后,不要忘了清空线段树

2、向下跑的deep[s]-2*deep[lca(s,i)]可能会产生负数,所以全体后移一定长度,root[]数组开大

我后移了2*n,那么root[]数组要开3倍

代码如下:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=300000+233;
int n,m,father[N],son[N],dep[N],bl[N];
int siz,id[N],ans[N];
int in[N],out[N],watch[N];
int front[N],nxt[N<<1],to[N<<1];
int root[N*3],ls[N*10],rs[N*10],sum[N*10];
int tot,cnt;

struct Node {
	int s,t;
	int lca;
}r[N];

void add_edge(int u,int v) {
	to[++cnt]=v;
	nxt[cnt]=front[u];
	front[u]=cnt;
}

void add(int u,int v) {
	add_edge(u,v);
	add_edge(v,u);
}

void dfs1(int now) {
	son[now]++;
	for(int i=front[now];i;i=nxt[i]) {
		if(to[i]==father[now]) continue;
		dep[to[i]]=dep[now]+1;
		father[to[i]]=now;
		dfs1(to[i]);
		son[now]+=son[to[i]];
		}
}

void dfs2(int now,int chain) {
	id[now]=++siz;
	in[now]=siz;
	bl[now]=chain;
	int y=0;
	for(int i=front[now];i;i=nxt[i]) {
		if(to[i]==father[now]) continue;
		if(son[to[i]]>son[y]) y=to[i];
	}
	if(!y) {
		out[now]=siz;
		return;
	}
	dfs2(y,chain);
	for(int i=front[now];i;i=nxt[i]) {
		if(to[i]==father[now]||to[i]==y) continue;
		dfs2(to[i],to[i]);
	}
	out[now]=siz;
}

int getlca(int u,int v) {
	while(bl[u]!=bl[v]) {
		if(dep[bl[u]]<dep[bl[v]]) swap(u,v);
		u=father[bl[u]];
	}
	return dep[u]<dep[v]?u:v;
}

void change(int &now,int l,int r,int pos,int w) {
	if(!pos) return;
	if(!now) now=++tot;
	sum[now]+=w;
	if(l==r) return;
	int mid=l+r>>1;
	if(pos<=mid) change(ls[now],l,mid,pos,w);
	else change(rs[now],mid+1,r,pos,w); 
}

int query(int now,int l,int r,int L,int R) {
	if(!now) return 0;
	if(l==L&&r==R) return sum[now];
	int mid=l+r>>1;
	if(R<=mid) return query(ls[now],l,mid,L,R);
	else if(L>mid) return query(rs[now],mid+1,r,L,R);
	else return query(ls[now],l,mid,L,mid)+query(rs[now],mid+1,r,mid+1,R);
}

void init() {
	tot=0;
	memset(ls,0,sizeof(ls));
	memset(rs,0,sizeof(rs));
	memset(sum,0,sizeof(sum));
	memset(root,0,sizeof(root));
}

int main() {
	freopen("running.in","r",stdin);
	freopen("running.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int u,v,i=1;i<n;i++) {
		scanf("%d%d",&u,&v);
		add(u,v);
	}
	for(int i=1;i<=n;i++) scanf("%d",watch+i);
	for(int i=1;i<=m;i++) scanf("%d%d",&r[i].s,&r[i].t);
	dfs1(1);
	dfs2(1,0); 
	for(int i=1;i<=m;i++) r[i].lca=getlca(r[i].s,r[i].t);
	for(int now,i=1;i<=m;i++) {
		now=dep[r[i].s];
		change(root[now],1,n,id[r[i].s],1);
		change(root[now],1,n,id[father[r[i].lca]],-1);
	}
	for(int i=1;i<=n;i++) ans[i]=query(root[dep[i]+watch[i]],1,n,in[i],out[i]);
	init();
	for(int now,i=1;i<=m;i++) {
		now=dep[r[i].s]-dep[r[i].lca]*2+n*2;
		change(root[now],1,n,id[r[i].t],1);
		change(root[now],1,n,id[r[i].lca],-1);
	}
	for(int i=1;i<=n;i++) ans[i]+=query(root[watch[i]-dep[i]+n*2],1,n,in[i],out[i]);
	for(int i=1;i<=n;i++) printf("%d ",ans[i]);
	return 0;
}


T3换教室:

这道题从题目上来看就知道是期望dp了,但是就noip来说,难度确实大了些:

首先,发现它的教室的数目小于300,而要求距离,显然Floyd 

考虑DP
设f[i][j][0..1]f[i][j][0..1],表示到第i节课,用了j次换课机会,第i节课是否使用机会(可能成功或没有成功)
每一个i只能从i-1转移到,即f[i][j+1][1]或f[i][j][0]=f[i−1][j][0]和f[i−1][j][1]……f[i][j+1][1]或f[i][j][0]=f[i−1][j][0]和f[i−1][j][1]……
省略号是可以互相转移到的状态的期望步数
转移就是两个状态之间所有可能情况的概率乘距离的和
如果想自己推转移方程就不要看下面

详细一点,分为四种情况考虑
c[i,j]是两个点间的最短距离
a[i]是原来的课室,b[i]是可以换课到的课室
p[i]是成功的概率
i-1是0,i是0
f[i][j][0]=f[i−1][j][0]+c[a[i−1],a[i]]f[i][j][0]=f[i−1][j][0]+c[a[i−1],a[i]]

i-1是1,i是0
f[i][j][0]=f[i−1][j][1]+p[i−1]∗c[b[i−1],a[i]]+(1−p[i−1])∗c[a[i−1],a[i]]f[i][j][0]=f[i−1][j][1]+p[i−1]∗c[b[i−1],a[i]]+(1−p[i−1])∗c[a[i−1],a[i]]

i-1是0,i是1
f[i][j+1][1]=f[i−1][j][0]+p[i]∗c[a[i−1],b[i]]+(1−p[i])∗c[a[i−1],a[i])f[i][j+1][1]=f[i−1][j][0]+p[i]∗c[a[i−1],b[i]]+(1−p[i])∗c[a[i−1],a[i])

i-1是1,i是1

f[i][j+1][1]=+p[i−1]∗p[i]∗c[b[i−1]][b[i]]+p[i−1]∗(1−p[i])∗c[b[i−1]][a[i]]+(1−p[i−1])∗p[i]∗c[a[i−1]][b[i]]+(1−p[i−1])∗(1−p[i])c[a[i−1]][a[i]]

状态转移方程可以说是我看过最长的了。。。代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define N 3000
using namespace std;
int n,m,u,v,t;
int a[N],b[N],c[310][310];
double p[N],f[N][N][2];

bool cnt(long long x,long long y) {
	return x>y;
}

int main() {
    freopen("classroom.in","r",stdin);
    freopen("classroom.out","w",stdout);
    scanf("%d%d%d%d",&n,&m,&v,&t);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    for(int i=1;i<=n;i++) scanf("%lf",&p[i]);
    memset(c,63,sizeof(c));
    for(int x,y,z,i=1;i<=t;i++) {
		scanf("%d%d%d",&x,&y,&z);
        c[x][y]=min(c[x][y],z);
		c[y][x]=c[x][y];
    }
    for(int i=1;i<=v;i++) c[i][i]=0;
    for(int k=1;k<=v;k++)
        for(int i=1;i<=v;i++)
            for(int j=1;j<=v;j++) c[i][j]=min(c[i][j],c[i][k]+c[k][j]);
    memset(f,127,sizeof(f));
    f[1][0][0]=f[1][1][1]=0;
    for(int i=2;i<=n;i++) {
        for(int j=0;j<=min(m,i);j++) {
            double jy1=c[b[i-1]][b[i]],jy2=c[a[i-1]][b[i]];
            double jy3=c[b[i-1]][a[i]],jy4=c[a[i-1]][a[i]];
            f[i][j][0]=min(f[i][j][0],f[i-1][j][0]+jy4);
            f[i][j+1][1]=min(f[i][j+1][1],f[i-1][j][0]+jy2*p[i]+jy4*(1-p[i]));
            if(!j) continue;
            f[i][j][0]=min(f[i][j][0],f[i-1][j][1]+p[i-1]*jy3+(1-p[i-1])*jy4);
            f[i][j+1][1]=min(f[i][j+1][1],f[i-1][j][1]+p[i-1]*p[i]*jy1+p[i-1]*(1-p[i])*jy3+(1-p[i-1])*p[i]*jy2+(1-p[i-1])*(1-p[i])*jy4);
        }
    }
    double ans=f[n][0][0];
    for(int i=1;i<=m;i++) ans=min(ans,min(f[n][i][0],f[n][i][1]));
    printf("%0.2lf",ans);
    return 0;
}
最后祝大家早日AK啦喵~

猜你喜欢

转载自blog.csdn.net/y554280697/article/details/80368571