Codeforces Round #620 (Div.2) (A - E)

Codeforces Round #620 (Div.2)

又是一次自闭场,我太菜了


A
距离除以速度。

B
由于 n < = 100 n<=100 n<=100,暴力 O ( n 2 ) O(n^2) O(n2)即可知道所有可能的配对回文情况。再检查是否可能有自回文的情况,如果有再在答案上加上一个即可。

C
在等待客户来的时候,可以对温度区间进行左右拓展: (x, y) -> (x - t, y + t)
当客户到了之后,将可到达的温度区间与客户的满意区间取交集: (x, y) ∩= (tx, ty);如果出现某次取交集后为空,则输出NO。


补题

D
我可能不配上分…昨晚应该在做梦吧…

给出1-n的排列相邻两个数之间的大小关系,试构造LIS最短的一种情况与LIS最长的一种情况。

先考虑最短情况:
因为对于每个连续上升区间,最短LIS长度不可能比其更小;所以我们就尝试构造该排列使得最短LIS长度就恰为最长的连续上升区间长度。为了达到这个目的,就要求对于一个上升区间,其之前所有的增区间都要严格地比它高,而之前的减区间对答案没有影响,这是因为如果取了之前减区间某个点,肯定不会比取这个V型谷底点更优,所以在减区间的情况我们需要保证构造不会越界[1, n]

容易想到这种情况的构造办法,不过需要预处理出来连续区间的长度(缩区间为点来优化时间复杂度)。用up记录当前最高可取的数值(初始为 n n n),now记录当前拐点,之后针对V型进行构造:对于一个V型结构,先移动到/的末端,从右往左填入up--,再移动到\,从左往右填入up--。如是反复操作即可。
注意当now == 1时要为起点初始化值,因为在之后的填值时拐点并不参与;当缩点后第一个单调区间为<时也要先从大到小填一遍,再回归到刚刚的V结构处理办法上。

再考虑最长情况:
显然将每一段连续增区间都连接起来就可以获得一个长度最长的LIS,与最短情况类似,无需考虑减区间可能带来的影响。

下面构造最长的LIS。
初始化ans[1] = 0,设置up = 1, down = -1,当当前比较符为小于号时填入up++,为大于号则填入down--

这样就可以完成一个相对高度 n n n、所有增区间皆可成功串联的 n n n个互不相同的数的排列。
再对所有元素加上-down就可以将其提升到绝对高度在[1, n]的排列。

const int maxn = 2e5 + 10;

struct node{
    char op; int len;
};

int n;
char s[maxn];
int ans[maxn];

vector<node> vec;
int top;

void least(){
    int p = 0;
    int up = n, now = 1;
    if(vec[0].op == '<'){
        for(int i = 0; i <= vec[0].len; i++) ans[vec[0].len + 1 - i] = up--;
        p = 1; now = vec[0].len + 1;
    }
    //V型
    while(p < top){
        if(p + 1 < top){
            for(int i = 0; i < vec[p + 1].len; i++)
                ans[now + vec[p].len + vec[p + 1].len - i] = up--;
            if(now == 1) ans[now] = up--;
            for(int i = 1; i <= vec[p].len; i++)
                ans[now + i] = up--;
            now += vec[p].len + vec[p + 1].len;
        }
        else{
            if(now == 1) ans[now] = up--;
            for(int i = 1; i <= vec[p].len; i++) ans[now + i] = up--;
        }
        p += 2;
    }
    for(int i = 1; i <= n; i++) printf(i != n? "%d ": "%d", ans[i]);
    printf("\n");
}

void most(){
    int down = -1, up = 1;
    ans[1] = 0;
    for(int i = 1; i <= n - 1; i++){
        if(s[i] == '<') ans[i + 1] = up++;
        else ans[i + 1] = down--;
    }
    for(int i = 1; i <= n; i++) ans[i] += -down;
    for(int i = 1; i <= n; i++) printf(i != n? "%d ": "%d", ans[i]);
    printf("\n");
}

int main(){
//    Fast;
    int t; scanf("%d", &t);
    while(t--){
        vec.clear();
        scanf("%d", &n); scanf("%s", s + 1);
        int i = 1;
        while(i <= n - 1){
            char op = s[i]; int len = 1;
            while(i + 1 <= n - 1 && s[i] == s[i + 1]){
                len++; i++;
            }
            i++;
            vec.push_back({op, len});
        }
        top = (int)vec.size();
        least(); most();;
    }
    return 0;
}

昨晚在写的时候想到了最长LIS的这种漂亮构造办法,但是发现好像并不适用最短LIS,要求解最短LIS应该一定要缩点,但是当时已经没有多少时间了…发现的有点晚…

E
因为在树上对于每一对点来说其之间距离都唯一确定,所以当每条边都可以重复经过时,从点 a a a到点 b b b就可以花费 d i s a b + 2 k , k ∈ Z + dis_{ab}+2k, k∈Z^+ disab+2k,kZ+ 到达。如果加入了某条边后从 a a a点到 b b b点之间距离的奇偶性可以发生变化,那么就有可能在 k k k步后到达原本不能够到达的 b b b点。

所以只需要判断三种情况是否有一种满足 d i s a b + 2 t = k ( t ∈ Z + ) dis_{ab}+2t = k(t∈Z^+) disab+2t=k(tZ+)

  • a->b
  • a->x->y->b
  • a->y->x->b

因为不确定 a a a点到 b b b点怎么样经过Edge[x, y]最优,所以要对两种情况都考虑一下。

自己想到这里后发现自己居然不会在树上 O ( l o g n ) O(logn) O(logn)的求两点之间的距离…=. =

因为之前线段树练习中遇到过括号序(进栈记录一次出栈记录一次)好像可以用线段树快速处理两个点之间的距离…但是为了避免瞎搞而心态崩我还是去看了下正统的求两点距离…

LCA

参考博客: LCA算法解析-Tarjan&倍增&RMQ (写得很让我喜欢)

记dis数组为当前节点到根节点(不妨取为1)的距离,如果快速求得了两点的LCA就可以利用 d i s a b = d i s [ a ] + d i s [ b ] − 2 ∗ d i s [ l c a ] dis_{ab}=dis[a] + dis[b] - 2 * dis[lca] disab=dis[a]+dis[b]2dis[lca]得到两点的距离。
确实很有道理。

tarjan离线算法
其本质有点类似于括号序?
如果某个节点及其子树被遍历完毕,则将其与其父节点用并查集合并;如果当前搜索点是查询的一组LCA且另一个节点已被访问过,就可以确定这组点的LCA即为另一个节点的父节点(即并查集的根节点,也就是当前节点的一个正在被搜索祖父节点)。

显然这个父节点既是另一个已访问节点的祖父节点,又是当前搜索节点的一个祖父节点;且由于dfs序(括号序)的性质,这个节点也是第一个具有遍及当前节点的分支的父亲节点。那么这种离线做法的正确性就不言自明了。

倍增
其最本质的思想在于暴力往更浅的地方找祖先直到两个点相遇。因为其复杂度不可接受,所以考虑用二进制倍增优化。

假设要求 L C A ( x , y ) LCA(x, y) LCA(x,y)
若起始时 x x x y y y的深度不同,由于其深度差值一定可以用二进制表示, O ( l o g n ) O(logn) O(logn)地先将二者深度齐平。当深度齐平后如果x == y说明恰好已经找到了LCA(此时x或y为另一方的子树节点);不然,就需要不断地往上走直到成为其公共祖先的两个左右节点:如果走了 2 k 2^k 2k步后二者祖先相同,按照我们的约定这两个点不能相遇而只能成为孩子节点,所以应该在[1, 2 k 2^k 2k- 1]中寻找,缩小步伐重走;如果走了 2 k 2^k 2k步后祖先不同,那么这个位置一定没有到达要求的LCA,更新位置后缩小步伐继续走;如是反复,跳出循环后father[x] == father[y] = LCA(x, y)

欧拉序
我还是比较喜欢这种做法,毕竟前不久刚刚玩了一段时间的线段树

不同于括号序,在dfn序中当元素进栈时记录一次,被子节点回溯到后再记录一次。因为一共有n个元素,可能进栈n次,也只会回溯n - 1次(根节点不会回溯),所以总共的长度为2 * n - 1 (有待考证,应该没有问题?);因为有点虚所以还是使用dfn序的长度times - 1吧。

假设节点x在节点y之前被遍历(即left[x] < left[y]),那么有了欧拉序后只需要在该序列中的区间[left[x], left[y]]中找最浅的一个节点即可,这个节点一定就是 L C A ( x , y ) LCA(x, y) LCA(x,y).

如果当询问次数远大于节点个数时,可以用ST表优化查询;但是这道题由于查询和节点个数差不多,所以用线段树查询的效率也是差不多的,顺手点就写线段树了。

1Try(还是很舒服的)

#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid ((l + r) >> 1)

const int maxn = 1e5 + 10;

struct star{
    int to, next;
}edge[maxn << 1];

int n, top = 0;
int head[maxn];

void add(int u, int v){
    edge[top].to = v;
    edge[top].next = head[u];
    head[u] = top++;
}

int dis[maxn];
int vis[maxn], dfn[maxn << 1], left[maxn << 1], right[maxn << 1];
int times = 1;
void dfs(int start){
    vis[start] = 1; dfn[times] = start; left[start] = times; times++;
    for(int i = head[start]; ~i; i = edge[i].next){
        if(!vis[edge[i].to]){
            vis[edge[i].to] = 1;
            dis[edge[i].to] = dis[start] + 1;
            dfs(edge[i].to);
            dfn[times++] = start;
            
        }
    }
    right[start] = times - 1;
}

int st[maxn << 3];

void up(int p){
    if(dis[st[ls]] < dis[st[rs]]) st[p] = st[ls];
    else st[p] = st[rs];
}

void build(int p, int l, int r){
    if(l == r){
        st[p] = dfn[l];
        return;
    }
    build(ls, l, mid); build(rs, mid + 1, r);
    up(p);
}

int query(int p, int l, int r, int L, int R){
    if(L <= l && r <= R){
        return st[p];
    }
    int ans = 0, temp;
    if(L <= mid){
        temp = query(ls, l, mid, L, R);
        if(dis[temp] < dis[ans]) ans = temp;
    }
    if(R > mid){
        temp = query(rs, mid + 1, r, L, R);
        if(dis[temp] < dis[ans]) ans = temp;
    }
    return ans;
}

void de(){
    puts("DFN:"); fro(int i = 1; i < times; i++) printf("%d ", dfn[i]); printf("\n");
    for(int i = 1; i <= n; i++) printf("%d: %d %d    dis: %d\n", i, left[i], right[i], dis[i]);
}

int getdis(int x, int y){
    int L, R;
    if(left[x] < left[y]){
        L = left[x]; R = left[y];
    }
    else{
        L = left[y]; R = left[x];
    }
    return dis[x] + dis[y] - 2 * dis[query(1, 1, 2 * n - 1, L, R)];
}

int main(){
//    Fast;
    memset(head, -1, sizeof head);
    scanf("%d", &n);
    for(int i = 0, x, y; i < n - 1; i++){
        scanf("%d %d", &x, &y);
        add(x, y); add(y, x);
    }
    dis[0] = 0x3f3f3f3f;
    dfs(1);
//    de();
    build(1, 1, times - 1);
    
    int m; scanf("%d", &m);
    int x, y, a, b, k;
    for(int i = 0; i < m; i++){
        scanf("%d%d%d%d%d", &x, &y, &a, &b, &k);
        int flag = 0, temp = 0;
        temp = getdis(a, b);
        if(k >= temp && (k - temp) % 2 == 0) flag = 1;
        
        temp = getdis(a, x) + 1 + getdis(y, b);
        if(k >= temp && (k - temp) % 2 == 0) flag = 1;
        
        temp = getdis(a, y) + 1 + getdis(x, b);
        if(k >= temp && (k - temp) % 2 == 0) flag = 1;
        
        puts(flag? "YES": "NO");
    }
    
    return 0;
}

好像ios:base(在iostream里)有个啥玩意的标识符就是left,导致我得把using namespace std关了才能避免重新取名字…emm



补完题感觉D和E不算特别难的题目…尽管场上D浪费了90分钟也没想出来,E题发现至今居然都不会树上两点的距离(我忘补坑了)…加上自己对这些算法的实现还不够娴熟,可能在时间上也不是很来得及做完…确实菜,不配上分…

明天要开始上网络课了,寒假算是正式过了,感觉和一个多月前一样菜… qwq

继续加油吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值