NOIP2017模拟赛(5) 总结

a 最远

题目描述

奶牛们想建立一个新的城市.它们想建立一条长度为N (1 <= N <= 1,000,000)的 主线大街,然后建立K条 (2 <= K <= 50,000)小街, 每条小街的尽头有一间房子(小街的其它位置没有房子).每条小街在主线大街的P_i 处分支,(0 <= P_i <= N) , 小街的长度是 L_i (1 <= L_i <= 1,000,000).FJ想知道最远的两个房子之间的距离是多少。


输入格式

  • 第1行: 两个整数: N 和K.
  • 第2..K+1行: 每行两个整数P_i、L_i. 对应着一条小街。

输出格式

  • 一行:最远的两个房子之间的距离.

输入样例

5 4
5 6
2 2
0 3
2 7


输出样例 1866.out

16

输入解释:
主线大街长度是5,有4条小街,分别位于距离主线大街 0、2、 2、 5 处。这4条小街的长度分别是3、 2、 7、 6. 注意:主线大街的同一个地点可以有多条小街.
输出解释:
房子1 和房子 4 的距离最远,是16。


解(song)题(fen)思路(找树的直径/O(n)枚举)

这题就是一道sb枚举题,排完序后,两个屋子之间的距离为 st[i].l+st[j].l+st[i].pst[j].p(j<i) 我们枚举 i ,然后开个变量记录st[j].lst[j].p的最大值并维护答案就行了。
然而我这个智障考试时不知到在想些什么,发现这就是个求树的直径。然后就写了两个dfs,拍了挺久,觉得稳了,然后一测dfs爆栈了(lj windows)。然后就送分了。。上OJ上一提交就过了(linux)。。

为什么要将简单的问题复杂化啊,树和图学傻了吧。。。


代码(单调枚举)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <cstring>
#define N 50010

using namespace std;

int n, ans;
struct Data{
    int p, l;
    bool operator < (const Data& A) const{return p < A.p;}
}st[N];

int main(){

    freopen("a.in", "r", stdin);
    freopen("a.out", "w", stdout);

    scanf("%d%d", &n, &n);

    for(int i = 1; i <= n; i++)  scanf("%d%d", &st[i].p, &st[i].l);
    sort(st+1, st+n+1);

    int far = st[1].l - st[1].p, ans = 0;
    for(int i = 2; i <= n; i++){
      ans = max(ans, st[i].l+st[i].p+far);
      far = max(far, st[i].l-st[i].p);
    }

    printf("%d\n", ans);

    return 0;
}

代码(dfs)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <algorithm>
#define N 1000010
#define K 50010

using namespace std;

int n, k, d[K<<1];
struct Data{
    int p, l;
    bool operator < (const Data& A) const{return p < A.p;}
}st[K];

int cur = -1, head_p[K<<1];
struct Tadj{int next, obj, len;} Edg[K<<2];

void Insert(int a, int b, int c){
    cur ++;
    Edg[cur].next = head_p[a];
    Edg[cur].obj = b;
    Edg[cur].len = c;
    head_p[a] = cur;
}

void dfs(int root, int fa){
    for(int i = head_p[root]; ~ i; i = Edg[i].next){
      int v = Edg[i].obj, l = Edg[i].len;
      if(v == fa)  continue;
      d[v] = d[root] + l;
      dfs(v, root);
    }
}

int main(){

    freopen("a.in", "r", stdin);
    freopen("a.out", "w", stdout);


    scanf("%d%d", &n, &k);

    for(int i = 1; i <= k; i++)  scanf("%d%d", &st[i].p, &st[i].l);

    sort(st+1, st+k+1);


    for(int i = 1; i <= k; i++)  head_p[i] = head_p[i+k] = -1;

    for(int i = 1; i <= k; i++){
      Insert(i, i+k, st[i].l);
      Insert(i+k, i, st[i].l);
    }

    int last = st[1].p;
    for(int i = 2; i <= k; i++){
      int dis = st[i].p - last;
      Insert(i-1+k, i+k, dis);
      Insert(i+k, i-1+k, dis);
      last = st[i].p;
    }

    d[1] = 0;
    dfs(1, 0); 

    int Max = 0, aim;
    for(int i = 1; i <= k*2; i++)  if(d[i] > Max){
      Max = d[i];
      aim = i;
    }
    d[aim] = 0;
    dfs(aim, 0);

    Max = 0;

    for(int i = 1; i <= k*2; i++)  Max = max(Max, d[i]);

    printf("%d\n", Max);

    return 0;
}

b 01游戏

题目描述

有一种游戏, 刚开始有A 个0和B个 1. 你的目标是最后变成A+B个1.
每一次,你选中任意K个数字, 把他们的值取反(原来是0的变1, 原来是1的变0).
请问至少需要多少次才能达到目标?假如不可能达到目标,就输出-1.


输入格式

多组测试数据.
第一行是一个整数: nG, 表示有nG组测试数据. 1 <= nG <= 5.
每组测试数据的格式如下:
第一行: 三个整数: A 、B 、K 。 1 <= A、B、K <= 100,000.


输出格式

共nG行,每行一个整数。


这里写图片描述


解题思路(数学题/宽搜+Splay)

这是一到神奇的题目,各种神奇的作法。本人在考场上认为这是一道数学题。于是想各种作法,抽屉原理,扩展欧几里德,搞来搞去,发现这些好像都没什么用,情急之下,写了个宽搜。
期望得分:?? 实际得分:28

这题我们可以有两种(以上作法),先讲朴素(暴力)的宽搜+数据结构优化。我们记当前有 A 个0的状态下操作次数为f[A],如果我们直接宽搜, O(K) 的转移比超时无疑。但我们对于一个状态 A ,能准确的算出它能转移到的一个连续区间的上下界。这个区间就是[abs(nowK),min(now+K,((A+B)<<1)Know)].(自己手推一下)
值得注意的是能转移的状态并不是这区间内的所有元素,而是里面的全部奇数或偶数(显然),这根区间的端点的奇偶性有关。而每个状态最多被转移一次。所以我们如果在转移后能将这个区间标记为转移过或者甚至将整个区间删去,那就能保证每个状态只访问一次,这样转移的时间复杂度就变成了 O(A+B)
然后如何标记或删除就可以使用线段树或splay,本人认为splay较好写,就写了splay。

如果用splay的话,就建两棵splay(一奇一偶),然后将要放入队列的区间翻转到根的右儿子的左子树。然后dfs一遍放入队列后就直接删除掉。
(一开始 A 要删掉)。我们不能直接搜区间的左右端点的前面或后面的点,而要找左端点的前驱翻到根,右端点的后继翻到根的右儿子。为什么呢?因为我们删掉一些区间后就不保证左端点的前一个还在拉。这样,我们保证在splay上区间的连续性和转移的奇偶性相同,就可以直接用裸的splay解决了,连打标记都不用。我考场上没写出来,证明我对splay极不熟练啊。。人家Kscla大神LCT都学得炉火纯青了,我还是找个坑把自己埋了吧。。。
时间复杂度:O(NlogN),N=A+B.

下面开始讲数学:
我们按照数学思想,进行如下推倒:
一开始设前面 A 个位置为0,后面B个位置为1,每个位置的值用 ei 表示, 令 n=A+B
Ti 为第 i 个位置的翻转次数,并设进行了x次操作,有

xK=i=1nTi  

我们设 Si 表示第 i 个位置进行了Si次二次翻转,因为翻转两次抵消,为了使所有的位置均为1, Ti Si 必须满足
Ti=2Si+[ei=0]  

联立①②得,
xK=A+21nsi  

xKA  

在这里对③可理解为在某些位置进行了二次翻转,然后在 A 个位置翻一次就达到全为1的状态,这就意味着,从③到目标状态是等价的,这是解决问题的突破点。
n1si观察可知道
对于 ei=1 的位置,二次翻转的次数不超过 x2
对于 ei=0 的位置,二次翻转的次数不超过 x12 ,因为最后一次翻转不能属于二次翻转中的一次。
假设所有的二次翻转次数都达到最大,那也必须满足
(xKA)/2=1nsiAx12+Bx2  

于是,满足④和⑤的最小的 x 即是答案。x的范围是 [0,A+B] 。超过范围就是-1了。
首先④和⑤是答案的必要条件,而由③中的等价转换又可知道这也是充分条件,于是问题就解决了。
时间 O(n)
其实还有比其更优秀神奇数学的 O(1) 作法,这里就不提了(我也不会)。


代码(宽搜+splay)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cmath>
#define N 200010

using namespace std;

int nG, A, B, K;
int head, tail, q[N], cur, f[N];

struct Tnode{
    Tnode *son[2], *fa;
    int val;
    int Get_d(){return fa->son[1] == this;}
    void Connect(Tnode *now, int d){(son[d] = now)->fa = this;}
}tree[N], *Root[2];

Tnode *NewTnode(){
    tree[cur].son[0] = tree[cur].son[1] = NULL;
    return tree+cur++;
}

Tnode *Build(int L, int R, Tnode *last){
    if(L > R)  return NULL;
    Tnode *now = NewTnode();
    int mid = (L + R) >> 1;
    if((L&1) ^ (mid&1)) mid ++;
    now->val = mid;
    now->fa = last;
    now->son[0] = Build(L, mid-2, now);
    now->son[1] = Build(mid+2, R, now);
    return now;
}

void Zig(Tnode *now, Tnode *&tag){
    Tnode *last = now->fa;
    int d = now->Get_d();
    if(now->son[!d])  last->Connect(now->son[!d], d);
    else  last->son[d] = NULL;
    if(last == tag){
      now->fa = tag->fa;
      tag = now;
    }
    else  last->fa->Connect(now, last->Get_d());
    now->Connect(last, !d);
}

void Splay(Tnode *now, Tnode *&tag){
    Tnode *last;
    while(now != tag){
      last = now->fa;
      if(last != tag) (last->Get_d() ^ now->Get_d()) ? Zig(now, tag) : Zig(last, tag);
      Zig(now, tag);
    }
}

int Find_pre(Tnode *now, int x, int pre){
    if(!now)  return pre;
    if(now->val < x)  return Find_pre(now->son[1], x, now->val);
    else  return Find_pre(now->son[0], x, pre);
}

int Find_suc(Tnode *now, int x, int suc){
    if(!now)  return suc;
    if(now->val > x)  return Find_suc(now->son[0], x, now->val);
    else  return Find_suc(now->son[1], x, suc);
}

void Work(Tnode *now, int val, Tnode *&tag){
    if(!now)  return;
    if(now->val == val)  Splay(now, tag);
    else if(now->val > val)  Work(now->son[0], val, tag);
    else  Work(now->son[1], val, tag);
}

void Trans(Tnode *now, int val){
    if(!now)  return;
    q[++tail] = now->val;
    f[now->val] = val;
    Trans(now->son[0], val);
    Trans(now->son[1], val);
}

int main(){

    freopen("b.in", "r", stdin);
    freopen("b.out", "w", stdout);

    scanf("%d", &nG);

    while(nG --){
      scanf("%d%d%d", &A, &B, &K);

      if(!A){  
        printf("0\n");
        continue;
      }

      cur = 0;

      int L = -2, R = A + B + 2;
      R -= R & 1;
      Root[0] = Build(L, R, NULL);
      L = -1;  R = A + B + 2;
      R -= !(R & 1);
      Root[1] = Build(L, R, NULL);

      int x = A & 1;
      Work(Root[x], A-2, Root[x]);
      Work(Root[x], A+2, Root[x]->son[1]);
      Root[x]->son[1]->son[0] = NULL;


      bool sol = false;
      q[head = tail = 0] = A;
      f[A] = 0;
      while(head <= tail){
        int now = q[head++];
        if(now == 0){
          printf("%d\n", f[now]);
          sol = true;
          break;
        }

        int low = abs(now-K), high = min(now+K, ((A+B)<<1)-K-now);
        if(low > high)  continue;
        x = low & 1;
        low = Find_pre(Root[x], low, 0);
        high = Find_suc(Root[x], high, 0);

        Work(Root[x], low, Root[x]);
        Work(Root[x], high, Root[x]->son[1]);
        Trans(Root[x]->son[1]->son[0], f[now]+1);
        Root[x]->son[1]->son[0] = NULL;
      }
      if(!sol)  printf("%d\n", -1);
    }

    return 0;
}

代码(O(n)数学)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <cstdlib>
using namespace std;

int nG;
long long A, B, K, n;
int main(){

    freopen("b.in", "r", stdin);
    freopen("b.out", "w", stdout);

    scanf("%d", &nG);
    while(nG --){
      scanf("%lld%lld%lld", &A, &B, &K);
      for(n = 0; n <= A + B; n++)
        if(A == 0 || (K<=A+B && n*K-A>=0 && !((n*K-A)&1) && (n*K-A)/2<=A*((n-1)/2)+B*(n/2)))  break;
      if(n <= A + B)  printf("%lld\n", n);
      else  printf("-1\n");
    }

    return 0;
}

c bst计数

题目描述

相信大家对二叉查找树都很熟悉了,现在给你N个整数的序列,每个整数都在区间[1,N]内,且不重复。现在要你按照给定序列的顺序,建立一个二叉查找树,把第一整数作为根,然后依次插入后面的整数。
每个结点X的插入过程其实就是模拟下面的 insert(X, root)过程:

这里写图片描述

你要求的是:每次把序列的一个整数插入到二叉查找数后,当目前为止计数类加器C的值是多少?请把它输出。注意:第一次插入根,计数器C的值是0,你可以理解为插入根是不执行insert()操作的,其后每插入一个结点,C都类加,也就是每次进入过程insert( number X, node N ),都会执行increase the counter C by 1,使得C不断增大。


输入格式

第一行:一个整数:N, 表示序列有多少个整数。 1 <= N <=300000
接下来有N行,每行一个整数X, X在区间[1,N]内,且不重复,这N个整数就组成了一个有序的序列。


输出格式

N行,每行一个整数,第一行表示你把序列的第一个数插入到二叉查找树后,当前计数器C的值是多少。 50%的数据:N <= 1000。


这里写图片描述


解题思路(treap)

暴力是标算,n方得50。
我们如果对bst和找前驱后继过程十分熟悉就可以发现,每一次插入x必然是插在x的前驱或后继的下一个,而且是深度大的那一个的下一个。
我们通过画图来认识一下:

这里写图片描述

在这张图中,x=4被插进了其前驱3的右儿子中,因为其前驱3的深度大于其后继5的深度。如果可以插在5的左边的话,就意味着5的左边没东西,然而是有3的。插入其后继的左儿子也是同理。
不过以上只是认识,证明的话,需要知道以下性质:在一个无重复节点的bst中,一个数的前驱同其后继是有祖先关系的
假设其前驱同后继没有祖先关系的话,那它们必有一个不同于其二的LCA,如图

这里写图片描述

这样对与任意一个x,必然其前驱或后继是LCA了。所以前驱和后继必然有祖先关系,于是我们如果选深度较小的那个插入就必然对应方向的子树中发现另一个并插入其后。
所以我们开一个treap,维护每个节点在bst中的深度,就是在旋转时保持深度不变,然后每次插入x就查询前驱和后继,得到当前的深度,再直接插入treap中(在treap中的位置不重要,对原来深度无影响),保存深度就行了。
时间 O(nlogn)
注意开long long。
其实还有 O(n) 的更厉害的作法,留坑待填


代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#define N 300010

using namespace std;

typedef long long LL;
LL cnt;
int n, x;
int cur;
struct Treap{
    int val, fix;
    LL dep;
    Treap *L, *R;
    inline void Clear(){L = R = NULL;}
}Node[N], *Root;

inline Treap *NewTnode(){
    Node[cur].Clear();
    return Node+cur++;
}

void Treap_R_Rot(Treap *&a){
    Treap *b = a->L;
    a->L = b->R;
    b->R = a;
    a = b;
}

void Treap_L_Rot(Treap *&a){
    Treap *b = a->R;
    a->R = b->L;
    b->L = a;
    a = b;
}

void Insert(Treap *&p, int val, LL dep){
    if(!p){
      p = NewTnode();
      p->val = val;
      p->fix = rand();
      p->dep = dep;
    }
    else if(val < p->val){
      Insert(p->L, val, dep);
      if(p->L->fix < p->fix)  Treap_R_Rot(p);
    }
    else{
      Insert(p->R, val, dep);
      if(p->R->fix < p->fix)  Treap_L_Rot(p);
    }
}

LL find_pre(Treap *p, int val, LL now){
    if(!p)  return now;
    if(p->val < val)  return find_pre(p->R, val, p->dep);
    else  return find_pre(p->L, val, now);
}


LL find_suc(Treap *p, int val, LL now){
    if(!p)  return now;
    if(p->val > val)  return find_suc(p->L, val, p->dep);
    else  return find_suc(p->R, val, now);
}

int main(){

    freopen("c.in", "r", stdin);
    freopen("c.out", "w", stdout);

    scanf("%d", &n);
    Root = NULL;
    while(n --){
      scanf("%d", &x);
      LL A = find_pre(Root, x, -1), B = find_suc(Root, x, -1), C = max(A, B) + 1;
      printf("%lld\n", cnt += C);
      Insert(Root, x, C);
    }

    return 0;
} 

总结

这次考得不太好,不到200分。然而Kscla神犇已经AK了。。。%%%
而且令我无语的是第一题windows下的爆栈神话,但愿记住这个教训,想题不能想复杂。还有就是提高做题速度,搞的第三题都没做。还一定要安排好做题的策略(这是我的硬伤),第二题想了很久最后还是没搞出来,不如直接去想第三题。我的数据结构果真还是太菜了。。。要多写多练,经常复习,以我的记忆,很快就忘了很多了。。。


这里写图片描述

吾王剑之所指,吾等心之所向。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值