hihocoder #1058 Combination Lock

传送门

时间限制:10000ms
单点时限:1000ms
内存限制:256MB

描述

Finally, you come to the interview room. You know that a Microsoft interviewer is in the room though the door is locked. There is a combination lock on the door. There are N rotators on the lock, each consists of 26 alphabetic characters, namely, 'A'-'Z'. You need to unlock the door to meet the interviewer inside. There is a note besides the lock, which shows the steps to unlock it.

Note: There are M steps totally; each step is one of the four kinds of operations shown below:

Type1: CMD 1 i j X: (i and j are integers, 1 <= i <= j <= N; X is a character, within 'A'-'Z')

This is a sequence operation: turn the ith to the jth rotators to character X (the left most rotator is defined as the 1st rotator)

For example: ABCDEFG => CMD 1 2 3 Z => AZZDEFG

Type2: CMD 2 i j K: (i, j, and K are all integers, 1 <= i <= j <= N)

This is a sequence operation: turn the ith to the jth rotators up K times ( if character A is turned up once, it is B; if Z is turned up once, it is A now. )

For example: ABCDEFG => CMD 2 2 3 1 => ACDDEFG

Type3: CMD 3 K: (K is an integer, 1 <= K <= N)

This is a concatenation operation: move the K leftmost rotators to the rightmost end.

For example: ABCDEFG => CMD 3 3 => DEFGABC

Type4: CMD 4 i j(i, j are integers, 1 <= i <= j <= N):

This is a recursive operation, which means:

If i > j:
	Do Nothing
Else:
	CMD 4 i+1 j
	CMD 2 i j 1

For example: ABCDEFG => CMD 4 2 3 => ACEDEFG

输入

1st line:  2 integers, N, M ( 1 <= N <= 50000, 1 <= M <= 50000 )

2nd line: a string of N characters, standing for the original status of the lock.

3rd ~ (3+M-1)th lines: each line contains a string, representing one step.

输出

One line of N characters, showing the final status of the lock.

提示

Come on! You need to do these operations as fast as possible.

 

样例输入
7 4
ABCDEFG
CMD 1 2 5 C
CMD 2 3 7 4
CMD 3 3
CMD 4 1 7
样例输出
HIMOFIN

Analysis:

CMD 1CMD 2是裸的线段树。CMD 4看起来很“吓人”,仔细分析一下不难发现CMD 4 i j是将密码锁的i位旋转(turn up)1次,第i+1位旋转2次,第i+1位旋转3次,...这也可以用线段树维护。 为了表述方便,我们用delta[i]表示位置i上由CMD 4造成的turn up次数。做法是将线段树的每个节点(每个节点都代表一个区间(L, R))增添两个attribute:一个是delta,用来记录delta[L];另一个是inc,用来记录该区间内delta[i+1]-delta[i]的值。应当注意到当对线段树上的某个区间(节点)进行第1次CMD 4操作后,其delta值是1,inc值是1;接着进行第2次操作后,其delta值是2,inc值是2。下面考虑CMD 3CMD 3 k 实际上是将字符串(即尚在构造中的密码)循环左移k位,比如对"ABCDEFG"进行CMD 3 3操作后,它变成"DEFGABC"。这种操作似乎是和线段树的精神相违背的,其实不然。线段树维护始终都是未经过移位操作的串,若原串已经循环累计左移s位了,我们只要将对应于当前(新)串的操作区间循环右移s位(注意:右移后[L, R]可能分裂为两段),之后再放到线段树里更新。

Implementation:

先贴代码,以后再填坑。
#include <bits/stdc++.h>
using namespace std;

const int N(5e4+5);
int same[N<<2], rot[N<<2], delta[N<<2], inc[N<<2];
char s[N];

void build(int id, int L, int R){
    if(L==R){same[id]=s[L]-'A'; return;}
    int mid=(L+R)>>1;
    same[id]=-1;
    build(id<<1, L, mid);
    build(id<<1|1, mid+1, R);
}

void CLEAR(int id, int v){
    same[id]=v, rot[id]=delta[id]=inc[id]=0;
}

void push_rot(int s, int f){
    rot[s]+=rot[f], rot[s]%=26;
}

void push_inc(int s, int f, int d){
    delta[s]+=d, inc[s]+=inc[f], delta[s]%=26, inc[s]%=26;
}

void push_down(int id, int L, int R){
    int ls=id<<1, rs=ls|1;
    if(~same[id]) CLEAR(ls, same[id]), CLEAR(rs, same[id]), same[id]=-1;
    if(rot[id]) push_rot(ls, id), push_rot(rs, id), rot[id]=0;
    int mid=(R+L)>>1, t=delta[id]+(mid+1-L)*inc[id];
    push_inc(ls, id, delta[id]), push_inc(rs, id, t%26), delta[id]=inc[id]=0;
}

int query(int id, int L, int R, int p){
    if(~same[id]) return (same[id]+rot[id]+delta[id]+(p-L)*inc[id])%26;
    push_down(id, L, R);    //error-prone
    int mid=(L+R)>>1;
    if(p<=mid) return query(id<<1, L, mid, p);
    return query(id<<1|1, mid+1, R, p);
}

void SET(int id, int L, int R, int l, int r, int v){
    if(l<=L && R<=r){
        CLEAR(id, v);
        return;        
    }
    push_down(id, L, R);
    int mid=(L+R)>>1;
    if(l<=mid) SET(id<<1, L, mid, l, r, v);
    if(r>mid) SET(id<<1|1, mid+1, R, l, r, v);
}

void ROTATE(int id, int L, int R, int l, int r, int v){
    if(l<=L && R<=r){rot[id]+=v, rot[id]%=26; return;}
    push_down(id, L, R);
    int mid=(L+R)>>1;
    if(l<=mid) ROTATE(id<<1, L, mid, l, r, v);
    if(r>mid) ROTATE(id<<1|1, mid+1, R, l, r, v);
}

void INC_SHIFT(int id, int L, int R, int l, int r, int v){
    if(l<=L && R<=r){
        delta[id]+=v+L-l;    //error-prone
        inc[id]+=1;
        delta[id]%=26, inc[id]%=26; 
        return;
    }
    push_down(id, L, R);
    int mid=(L+R)>>1;
    if(l<=mid) INC_SHIFT(id<<1, L, mid, l, r, v);    //error-prone
    if(r>mid) INC_SHIFT(id<<1|1, mid+1, R, l, r, v);
}

int main(){
    int n, m;
    cin>>n>>m>>s;
    build(1, 0, n-1);
    char ch;
    int shift=0;    //number of left shift
    for(int t, l, r, k; m--; ){
        scanf("%*s%d", &t);
        if(t!=3) cin>>l>>r, l--, r--, l=(l+shift)%n, r=(r+shift)%n;
        if(t==1){
            cin>>ch, k=ch-'A';    //cin ignores leading spaces.
            if(l<=r) SET(1, 0, n-1, l, r, k);
            else SET(1, 0, n-1, l, n-1, k), SET(1, 0, n-1, 0, r, k);
        }
        else if(t==2){
            cin>>k;
            if(l<=r) ROTATE(1, 0, n-1, l, r, k);
            else ROTATE(1, 0, n-1, l, n-1, k), ROTATE(1, 0, n-1, 0, r, k);
        }
        else if(t==3){
            cin>>k, shift+=k, shift%=n;
        }
        else{
            if(l<=r) INC_SHIFT(1, 0, n-1, l, r, 1);
            else INC_SHIFT(1, 0, n-1, l, n-1, 1), INC_SHIFT(1, 0, n-1, 0, r, n-l+1);    
        }
    }
    for(int i=0; i<n; i++) putchar(query(1, 0, n-1, (i+shift)%n)+'A');
    puts("");
    return 0;
}

实现细节:

这道题代码量相对大一些,而且有些地方容易想不清楚。

先总结一下线段树:

线段树是用来维护区间上的修改(亦称 更新/modify/update)与查询(query)的。修改与查询都可分成两类:点修改,区间修改;点查询,区间查询。

其中区间修改往往要用到 lazy-tag 技巧。线段树节点维护的所有atrribute都是关于这个节点所对应的区间的,广义而言,都可看作区间的函数

\[f([L, R])\] 这些 attribute 记录的信息可分为两类,一类是该区间的某种属性(properties),另一类是对此区间(已经)进行的某些操作(operations),或者说该区间经历 (expierenced)的某些操作。

再说说这道题的实现:

线段树的每个节点所需的 atrribute,除了 Analysis 中提到的 delta, inc(用来记录该区间经历的CMD 4操作)之外,还有

  • same,用来记录该区间经历的CMD 1操作,我们用0~25代表'A'~'Z';
  • rot,用来记录该区间所经历的CMD 2操作;

 CMD 1操作会将该区间已经历的所有其他操作全部覆盖(清空)。

 

 
 
 
 
 
 
 
posted @ 2016-04-17 12:03  Pat  阅读(330)  评论(0编辑  收藏  举报