NOIP2016小结

来源:互联网 发布:sql删除表中的记录 编辑:程序博客网 时间:2024/05/14 18:11

嗯。。一下子离复赛结束已经过两个月了= =

今年补题比去年轻松多了。。不过还是等到bzoj贴出题目以后才开始补

毕竟能水点经验嘛,,何乐而不为~

一直想写点东西,,不然blog里全是题解。。。。。。。233

先放解题报告吧- -


玩具迷题(D1T1):

有n个玩具小人排成一圈,,每个玩具有自己的名字,已知他们的朝向(圈内或圈外),现在从1号小人开始,每次左数/右数ai个人,问m次操作后指向的那个小人是谁。n,m <= 1E5


solution:

朝向 + 转向决定顺时针or逆时针,直接模拟。。送分题就没什么好说的了

考场用xor写的代码= =能少写一点点if,233。。。

#include<iostream>#include<cstdio>#include<cstring>#include<vector>#include<queue>#include<algorithm>#include<cmath>using namespace std;const int maxn = 1E5 + 10;int n,m,now = 1,typ[maxn];char Name[maxn][20];int main(){#ifdef DMCfreopen("DMC.txt","r",stdin);#elsefreopen("toy.in","r",stdin);freopen("toy.out","w",stdout);#endifcin >> n >> m;for (int i = 1; i <= n; i++)scanf("%d%s",&typ[i],Name[i]);for (int i = 1; i <= m; i++){int a,s; scanf("%d%d",&a,&s);a ^= typ[now];if (a) {now += s;if (now > n) now -= n;}else{now -= s;if (now < 1) now = n + now;}}int len = strlen(Name[now]);for (int i = 0; i < len; i++) putchar(Name[now][i]);return 0;}

天天爱跑步(D1T2):

给出一棵树,每个节点在时刻wi会出现一名观察员,现在有m个玩家,他们从时刻0同时开始出发,沿着树上路径xi -> yi行走,固定每走一条边耗时为1,一个玩家能被一个观察员观察到,当且仅当该玩家在时刻wi到达节点i,,要求输出每位观察员能观察到的玩家数。n,m <= 3E5


solution:

考场上这题,,自己把自己搞得很混乱= =花了1h写+调试出一个错误的算法。。GG,于是直接跳去写T3(后来事实证明这很明智)。最后一点时间只想出40分部分分的算法(暴力 + Si == 1(树上前缀和)),然而。。。树上前缀和那个部分忘记输出了!!!结果最后只拿了20分。。无限血崩

考试的时候是不会线段树合并的。。这个东西是前段时间才学。。这里提供一个用线段树合并O(nlogn)的做法

只讨论lca(xi,yi)既不是xi也不是yi的情况,首先每条路径(xi,yi)拆成(xi,lca)(t,yi)其中t是(lca,y)往下走一步。在(xi,lca)段,若有节点k能够看到玩家i,那么L[x] - L[k] == w[k],L[i]为节点i的深度

同理,在(t,yi)段,若有节点k能看到玩家i,那么L[x] - L[lca] + L[k] - L[lca] == w[k]

两个式子分别移项一下,得到L[x] == w[k] + L[k],L[x] - 2*L[lca] == w[k] - L[k]

可以发现,右边的东西每个点有每个点的定值

考虑每个点维护两棵权值线段树,分别处理两种询问

如果我们能快速将子树信息递给父亲,那么对于每个点,在两棵线段树里分别找到对应定值,加起来,就是该点的答案了。

想要做到快速转移,一个不错的方案是用线段树合并,之前的一篇blog有转载详解该算法的论文,就不再赘述了。。然后这种操作是均摊O(nlogn)的复杂度

嗯,,常数注意写小一点,,苟蒻的代码常数略大,不过bzoj的评测机嘛。。~

#include<iostream>#include<cstdio>#include<algorithm>#include<cmath>#include<cstring>#include<vector>#include<queue>#include<set>#include<map>#include<stack>#include<bitset>#include<ext/pb_ds/priority_queue.hpp>using namespace std; const int maxn = 3E5 + 30;const int T = 100; int n,m,cnt,w[maxn],L[maxn],fa[maxn][20],c[maxn*T],ans[maxn]    ,lc[maxn*T],rc[maxn*T],rt1[maxn],rt2[maxn]; vector <int> v[maxn],Q1[maxn],Q2[maxn],D1[maxn],D2[maxn]; void Dfs1(int x,int from){    for (int i = 1; i < 20; i++) fa[x][i] = fa[fa[x][i-1]][i-1];    for (int i = 0; i < v[x].size(); i++)    {        int to = v[x][i];        if (to == from) continue;        L[to] = L[x] + 1; fa[to][0] = x; Dfs1(to,x);    }} int LCA(int p,int q){    if (L[p] < L[q]) swap(p,q);    for (int j = 19; j >= 0; j--)        if (L[p] - (1<<j) >= L[q])            p = fa[p][j];    if (p == q) return p;    for (int j = 19; j >= 0; j--)        if (fa[p][j] != fa[q][j])            p = fa[p][j],q = fa[q][j];    return fa[p][0];} int Quickfa(int x,int y){    for (int now = 0; y; y >>= 1,now++)        if (y&1) x = fa[x][now];    return x;} int Merge(int o1,int o2,int l,int r){    if (!o1) return o2;    if (!o2) return o1;    int ret = ++cnt;     c[ret] = c[o1] + c[o2];    if (l == r) return ret;    int mid = (l + r) >> 1;    lc[ret] = Merge(lc[o1],lc[o2],l,mid);    rc[ret] = Merge(rc[o1],rc[o2],mid+1,r);    return ret;} int Query(int o,int l,int r,int pos){    if (l == r) return c[o];    if (!c[o]) return 0;    int mid = (l + r) >> 1;    if (pos <= mid) return Query(lc[o],l,mid,pos);    else return Query(rc[o],mid+1,r,pos);} int Modify(int o,int pos,int l,int r,int z){    int ret = o?o:++cnt;    c[ret] = c[o] + z;    if (l == r) return ret;    int mid = (l + r) >> 1;    if (pos <= mid) lc[ret] = Modify(lc[ret],pos,l,mid,z);    else rc[ret] = Modify(rc[ret],pos,mid+1,r,z);    return ret;} void Dfs2(int x,int from){    for (int i = 0; i < v[x].size(); i++)    {        int to = v[x][i];        if (to == from) continue;        Dfs2(to,x);        rt1[x] = Merge(rt1[x],rt1[to],1,n);        rt2[x] = Merge(rt2[x],rt2[to],-2*n,n);    }         for (int i = 0; i < Q1[x].size(); i++)        rt1[x] = Modify(rt1[x],Q1[x][i],1,n,1);    for (int i = 0; i < Q2[x].size(); i++)        rt2[x] = Modify(rt2[x],Q2[x][i],-2*n,n,1);             if (1 <= w[x] + L[x] && w[x] + L[x] <= n)        ans[x] += Query(rt1[x],1,n,w[x] + L[x]);    if (-2*n <= w[x] - L[x] && w[x] - L[x] <= n)        ans[x] += Query(rt2[x],-2*n,n,w[x] - L[x]);             for (int i = 0; i < D1[x].size(); i++)        rt1[x] = Modify(rt1[x],D1[x][i],1,n,-1);    for (int i = 0; i < D2[x].size(); i++)        rt2[x] = Modify(rt2[x],D2[x][i],-2*n,n,-1);} int getint(){    char ch = getchar();    int ret = 0;    while (ch < '0' || '9' < ch) ch = getchar();    while ('0' <= ch && ch <= '9')        ret = ret*10 + ch - '0',ch = getchar();    return ret;} int main(){    #ifdef DMC        freopen("DMC.txt","r",stdin);    #endif         cin >> n >> m;    for (int i = 1; i < n; i++)    {        int x = getint(),y = getint();        v[x].push_back(y);        v[y].push_back(x);    }    for (int i = 1; i <= n; i++) w[i] = getint();    L[1] = 1; Dfs1(1,0);    while (m--)    {        int x = getint(),y = getint();        int lca = LCA(x,y);        if (lca == x)        {            Q2[y].push_back(L[x] - 2*L[lca]);            D2[lca].push_back(L[x] - 2*L[lca]);        }        else if (lca == y)        {            Q1[x].push_back(L[x]);            D1[lca].push_back(L[x]);        }        else        {            int t = Quickfa(y,L[y] - L[lca] - 1);            Q1[x].push_back(L[x]);            D1[lca].push_back(L[x]);            Q2[y].push_back(L[x] - 2*L[lca]);            D2[t].push_back(L[x] - 2*L[lca]);        }    }    Dfs2(1,0);    for (int i = 1; i < n; i++) printf("%d ",ans[i]); cout << ans[n];    return 0;}

换教室(D1T3):

牛牛在大学要上一共n节课,这些课程被安排在n个不同的时间段,且第i节课程可以选择在教室ai或教室bi完成。当然,完成一节课后牛牛要跑到下一间教室上下一节课,在教室之前跑来跑去是很累的。为了方便,牛牛可以申请第i节课在教室bi上(不申请的话默认在ai上课),可是,申请通过的期望是pi,而且,牛牛不能用超过m次申请。现在牛牛想知道一个最好的申请方案,使得牛牛上完n节课期望走的距离最小是多少,只需要知道这个期望距离就行了。(也就是可以看做,n间教室是在一个连通图内,边有边权) n <= 300


solution:

还好最后是设计出了正确的dp状态= =

不管怎么做此题,总是要在原图中跑来跑去,那当然每次都是走最短路最优啦。。也就是说肯定会多次询问两点最近距离,n又这么小,于是可以先一个floyed把所有两点间的距离都求出来

定义状态f[i][j][k]:前i节课程,用了j次申请,第i节是否用申请(01状态k),期望最短路

转移的话就是f[i-1][j-1][k1] -> f[i][j][k2]   f[i-1][j][k1] -> f[i][j][k2]

枚举k1,k2的情况,分类讨论如何转移即可(代码略长,要注意细节)

这里就以k1 = 0,k2 = 1示例,其它情况类似

既然k1 = 0,k2 = 1,那么转移只发生在f[i-1][j-1][0] -> f[i][j][1]
既然上一次没有用申请,那么上一次一定是在教室ai-1

而此次我们申请更换教室,意味着pi的概率成功,1-pi的概率失败

如果成功了那么这次就在教室bi上课,否则在教室ai

于是转移为f[i][j][1] = min(f[i][j][1],f[i-1][j-1][0]*((1-pi)*dis[ai-1][ai] + pi*dis[ai-1][bi]))

dis数组就是用floyed算法预处理的两点最短路了

#include<iostream>#include<cstdio>#include<cstring>#include<vector>#include<queue>#include<algorithm>#include<cmath>using namespace std;const int maxn = 2020;const int N = 303;typedef double DB;const DB INF = 1E16;const DB eps = 1E-9;const DB ONE = 1.00;const int Max = ~0U>>1;struct E{int to,w; E(){}E(int to,int w): to(to),w(w){}};DB f[maxn][maxn][2],K[maxn];int n,m,t,p,dis[N][N],R[maxn][2];vector <E> v[N];void Floyd(){for (int i = 1; i <= t; i++) dis[i][i] = 0;for (int k = 1; k <= t; k++)for (int x = 1; x <= t; x++)for (int y = 1; y <= t; y++){if (dis[x][k] == Max || dis[k][y] == Max) continue;dis[x][y] = min(dis[x][y],dis[x][k] + dis[k][y]);}}DB Dis(int x,int y){return (DB)(dis[x][y]);}int main(){#ifdef DMCfreopen("DMC.txt","r",stdin);#elsefreopen("classroom.in","r",stdin);freopen("classroom.out","w",stdout);#endifcin >> n >> m >> t >> p;for (int i = 0; i <= n; i++)for (int j = 0; j <= m; j++)for (int l = 0; l < 2; l++) f[i][j][l] = INF;f[0][0][0] = 0;for (int i = 1; i <= t; i++)for (int j = 1; j <= t; j++)dis[i][j] = Max;for (int i = 1; i <= n; i++) scanf("%d",&R[i][0]);for (int i = 1; i <= n; i++) scanf("%d",&R[i][1]);for (int i = 1; i <= n; i++) scanf("%lf",&K[i]);while (p--){int x,y,w; scanf("%d%d%d",&x,&y,&w);dis[x][y] = min(dis[x][y],w);dis[y][x] = min(dis[y][x],w);}Floyd();for (int i = 0; i < n; i++)for (int j = 0; j <= m; j++)for (int A = 0; A < 2; A++)for (int B = 0; B < 2; B++){if (fabs(f[i][j][A] - INF) <= eps) continue;if (!A && !B){DB &Nex = f[i+1][j][B];DB Now = Dis(R[i][0],R[i+1][0]);Now += f[i][j][A];Nex = min(Nex,Now);}else if (A && !B){DB &Nex = f[i+1][j][B];DB sum = 0,Now = f[i][j][A];DB X = Dis(R[i][0],R[i+1][0]);DB Y = Dis(R[i][1],R[i+1][0]);sum = (ONE - K[i])*(Now+X) + K[i]*(Now+Y);Nex = min(Nex,sum);}else if (!A && B){if (j == m) continue;DB &Nex = f[i+1][j+1][B];DB sum = 0,Now = f[i][j][A];DB X = Dis(R[i][0],R[i+1][0]);DB Y = Dis(R[i][0],R[i+1][1]);sum = (ONE-K[i+1])*(Now+X) + K[i+1]*(Now+Y);Nex = min(Nex,sum);}else if (A && B){if (j == m) continue;DB &Nex = f[i+1][j+1][B];DB sum = 0,Now = f[i][j][A];DB X = Dis(R[i][0],R[i+1][0]);DB Y = Dis(R[i][0],R[i+1][1]);sum = (ONE-K[i])*(ONE-K[i+1])*(Now+X) + (ONE-K[i])*K[i+1]*(Now+Y);X = Dis(R[i][1],R[i+1][0]);Y = Dis(R[i][1],R[i+1][1]);sum += K[i]*(ONE-K[i+1])*(Now+X) + K[i]*K[i+1]*(Now+Y);Nex = min(Nex,sum);}}DB Ans = INF;for (int i = 0; i <= m; i++)for (int j = 0; j < 2; j++)Ans = min(Ans,f[n][i][j]);if (Ans <= 0.001) printf("0.00");else printf("%.2lf",Ans);return 0;}

组合数问题(D2T1):

给定常数k,t,求  t组数据 n,m,t <= 2000


solution:

题目还给了组合数一般公式。。然而并没有什么用。。。

只需要用递推式O(n^2)预处理所有组合数,然后二维前缀和就行了

询问O(1)的。。。

#include<iostream>#include<cstdio>#include<cstring>#include<vector>#include<queue>#include<algorithm>#include<cmath>using namespace std;const int maxn = 2020;const int N = 2000;int n,m,k,t,sum[maxn],C[maxn][maxn],ans[maxn][maxn];void Work(int r,int c){if (c) ans[r][c] += ans[r][c-1];if (r) ans[r][c] += sum[c];if (!C[r][c]) ++ans[r][c],++sum[c];}int main(){#ifdef DMCfreopen("DMC.txt","r",stdin);#elsefreopen("problem.in","r",stdin);freopen("problem.out","w",stdout);#endifcin >> t >> k;for (int i = 0; i <= N; i++){C[i][0] = 1 % k; Work(i,0);for (int j = 1; j <= i; j++){C[i][j] = (C[i-1][j] + C[i-1][j-1]) % k;Work(i,j);}}while (t--){int x,y; scanf("%d%d",&x,&y);y = min(y,x);printf("%d\n",ans[x][y]);}return 0;}


蚯蚓(D2T2):

蛐蛐国蚯蚓泛滥。。于是国王请了一位神刀手来砍蚯蚓,然而,,蚯蚓的生命力是很顽强的。

总之砍不死。。。。每次神刀手会选择一条长度最长的蚯蚓,将其斩为两段。

假设当前选的蚯蚓长度为x,那么分为两段[x*p],x - [x*p]

其中[x]表示对x向下取整,0 < p < 1,常数,注意,即使长度为0,蚯蚓还是存在的= =

在砍断一条蚯蚓的同时,所有其它蚯蚓的长度增加q

现在要求,执行m次操作,然后输出一些参数。。(这里就不解释输出了,总之m次操作必须都执行完)

n <= 1E5,m <= 7*1E6


solution:

一个很直接的想法是,,上个堆维护,但是m巨大,绝对不可能允许带log的算法

实际上,,,蚯蚓的长度具有某种意义上的单调性。。具体如下

首先将所有蚯蚓按长度降序排序插入一个队列

记每次斩断的蚯蚓,[x*p]为左半段,x - [x*p]为右半段

第一次,将初始队列队首拿出来斩断,左半段进一个新队列,右半段另一个。。。。

如此往复,总之,每次斩的蚯蚓,一定是这三个队列队首蚯蚓中长度最长的那个

不断操作即可。。如果单纯维护队列,每个元素维护一个时间戳,就能知道长长了多少次了

至于这样为什么正确,,,因为先斩的蚯蚓的剩余部分一定不小于后斩的。

注意是在三个队列中分别成立(这也是为什么要维护三个队列而不是一个)

粗略证明下,先斩x再斩y

剩余部分[x*p] + q,[(y+q)*p],右半段类似,因为x > y,所以前者显然大于后者

#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<cmath>using namespace std; const int maxn = 1E5 + 10;typedef double DB; int n,m,q,u,v,t,now,a[maxn];DB p; struct data{    int ti,A; data(){}    data(int ti,int A): ti(ti),A(A){}    bool operator < (const data &B) const    {        return A + (now - ti - 1)*q < B.A + (now - B.ti - 1)*q;    }}; queue <data> Q[3]; int Get_Max(){    data G; G = data(now,-233);    int pos = 233;    for (int i = 0;i < 3; i++)        if (!Q[i].empty() && G < Q[i].front())            G = Q[i].front(),pos = i;    Q[pos].pop(); return G.A + (now - G.ti - 1)*q;} int main(){    #ifdef DMC        freopen("DMC.txt","r",stdin);    #endif         cin >> n >> m >> q >> u >> v >> t; p = (DB)(u)/(DB)(v);    for (int i = 1; i <= n; i++) scanf("%d",&a[i]);    sort(a + 1,a + n + 1);    for (int i = n; i; i--) Q[0].push(data(0,a[i]));    for (now = 1; now <= m; now++)    {        int len = Get_Max();        if (now % t == 0)        {            if (now + t > m) printf("%d",len);            else printf("%d ",len);        }        int x = floor((DB)(len)*p),y = len - x;        Q[1].push(data(now,x)); Q[2].push(data(now,y));    }    puts("");    for (int i = 1; i <= n + m; i++)    {        int len = Get_Max();        if (i % t == 0)        {            if (i + t > n + m) printf("%d",len);            else printf("%d ",len);        }    }    return 0;}


愤怒的小鸟(D2T3):

就是模拟这个游戏啦。。。给出一个二维平面,上面有一些点(猪),然后你要发射一些小鸟消灭他们。。

特别的,小鸟们的飞行轨迹总是经过原点的抛物线,如果碰到猪,小鸟也不会停下,继续前进

而猪如果被碰到就死了。。。。问最少需要多少只小鸟消灭所有的猪。。。

总共有n只猪,猪们的坐标都是正实数,n <= 18


solution:

正解是状压dp,f[op]:消灭集合op的猪需要多少只小鸟

转移的话,,可以预处理打到每两只猪的小鸟的轨迹可以打到的猪的集合。

那么转移的时候可以枚举确定抛物线的这两只猪进行转移。

这样毫无优化的裸dp是O(n^2*2^n)的。。。n再小也不能这样玩呀= =

不过,注意到杀猪的序顺并不影响案答。。。

可以强制让确定抛物线的两只小猪中的第一只为当前为杀死的小猪中编号最小的那头

这样就降到O(n*2^n)了。。。注意常数别写太大


不过。。。苟蒻博主看到此题的第一反应竟是搜索。。。爆搜

因为去年有斗地主。。。往年还有Mayan游戏这样的大题。。。

省赛前我就很自信地和我校选手说。。。。Noip感觉是每年都会有一道搜索大题的节奏呀= =

而且D1T3不是已经考过dp了吗。。怎么可能会考两个dp。。。。。于是考场就写了个搜索

但是。。惊喜的是,,,通过了所有数据点

除了最后一个数据1.4s略慢。。其它都是飞过去的= =

几个剪枝如下:

1.因为小鸟出发点为原点,所有再来两只小猪就能确定一条抛物线了,搜索的时候,对于每只小猪,就枚举他是新开一条抛物线(用一只新小鸟杀死他),和之前的某只小猪一起,合并成一条抛物线,或者是能够被之前的某条抛物线覆盖

2.注意到上述三个状态中,第三个总是最好的情况。虽然,题目说小鸟碰到的小猪就会死亡,但是不一定要这样操作。。小猪哪次死结果不都是一样么= =因此,对于每只小猪,如果之前一条已经确定的抛物线能覆盖他,那么就把他直接归给那条抛物线就好啦,不再进行别的讨论

3.注意到题中种种限制,迭代加深搜索是个不错的选择。

#include<iostream>#include<cstdio>#include<cstring>#include<vector>#include<queue>#include<algorithm>#include<cmath>using namespace std;typedef double DB;const int N = 20;const DB eps = 1E-9;DB x[N],y[N],a[N],b[N];int T,n,m,sum[N];bool Pass(int now,int t){DB g = a[t]*x[now]*x[now] + b[t]*x[now];return fabs(g - y[now]) <= eps;}bool Add(int now,int t){DB B = x[now]*a[t]*(x[now] - a[t]);if (fabs(B) <= eps) return 0;DB A = a[t]*y[now] - x[now]*b[t]; A /= B;if (A > 0.00 || fabs(A) <= eps) return 0;B = (y[now] - A*x[now]*x[now]) / x[now];a[t] = A; b[t] = B; return 1;}bool Dfs(int now,int k,int Max){if (k > Max) return 0;if (now > n) return 1;for (int i = 1; i <= k; i++)if (sum[i] > 1 && Pass(now,i)) return Dfs(now + 1,k,Max);for (int i = 1; i <= k; i++)if (sum[i] == 1){DB X = a[i],Y = b[i];bool ret = Add(now,i);if (!ret) continue; sum[i] = 2;ret = Dfs(now + 1,k,Max);if (ret) return ret;sum[i] = 1; a[i] = X; b[i] = Y;}a[++k] = x[now]; b[k] = y[now]; sum[k] = 1;return Dfs(now + 1,k,Max);}int main(){#ifdef DMCfreopen("DMC.txt","r",stdin);#elsefreopen("angrybirds.in","r",stdin);freopen("angrybirds.out","w",stdout);#endifcin >> T;while (T--){scanf("%d%d",&n,&m);for (int i = 1; i <= n; i++) scanf("%lf%lf",&x[i],&y[i]);int L = 1,R = n;if (m == 1){if (n % 3 == 0) R = n / 3 + 1;else R = n / 3 + 2;}if (m == 2){R = n - n / 3 + 1;}while (R - L > 1){int mid = (L + R) >> 1;if (Dfs(1,0,mid)) R = mid;else L = mid;}if (Dfs(1,0,L)) printf("%d\n",L);else printf("%d\n",R);}return 0;}


后记:

去年参赛的时候只拿了355分。。虽然也是省一,但是和自己的目标差得还很远。。。我是从初一开始学算法的,,不过现在看来,那时都是小打小闹罢了。生活在一个很穷的县级市,,就没办法指望有很好的环境攻一门竞赛了。。不过这一年,,风风雨雨也是坚持下来了。特地是凑到399篇博文再来填Noip题解的博文的坑,也就是说,这是第400篇博文了。把它看成是一个里程碑,希望这是一个新的开始吧!

今年的复赛,,拿了480分,虽有一些失误,但也算正常发挥吧。嗯,这个分在FJ弱省还是能排第15的。。还好wc、apio之类的门票算是拿到了。。嗯,,希望后来的日子里能再接再厉吧!想能混个省B队吧,,想去全国赛看看。。嗯,,这毕竟是自己的梦想来着。反正是做自己喜欢的事,总是不会后悔的!

啊。。语文功底不太好,这是第一次写不是题解的东西。。也不知写什么了,大概就这样吧!博主的博客名是CRZbulabula(取自自己的名字),cf的ID是Charming_Chen(取自段长的名字~哈哈),两个自己给自己取的“英文名”还是更喜欢后面那个~

最后,,希望碰巧看到这篇瞎逼逼题解的朋友,也能一起努力吧!努力的话也许能在NOI2017相见!

1 0
原创粉丝点击