zoukankan      html  css  js  c++  java
  • Codeforces Round #578 (Div. 2)

    题目链接:https://codeforces.com/contest/1200

    A - Hotelier

    送分题。

    B - Block Adventure

    一条简单的dp,注意负数。

    C - Round Corridor

    简单数论,随便弄个gcd除一下。

    D - White Lines

    题意:给出一个边长不超过2000的黑白格矩阵,有一个橡皮擦工具,可以把以(i,j)为左上端点的一个边长为k的正方形全部变成白格,求用恰好一次橡皮擦工具,最多拥有多少个全白行和全白列。

    题解:假如只看全白行,就把第i行的第一个黑格fi和最后一个黑格la找出来,那么当且仅当在[i-k+1,i]*[la-k+1,fi]中使用橡皮擦工具可以使这一行变成全白,用个差分暴力一下可以维护。同理可以算出全白列的情况,然后把两次差分的结果合并起来。

    char g[2005][2005];
    int dr[2005][2005];
    int dc[2005][2005];
    int ans[2005][2005];
     
    void test_case() {
        int n, k;
        scanf("%d%d", &n, &k);
        for(int i = 1; i <= n; ++i)
            scanf("%s", g[i] + 1);
        for(int i = 1; i <= n; ++i) {
            int fi = 0, la = 0;
            for(int j = 1; j <= n; ++j) {
                if(g[i][j] == 'B') {
                    if(fi == 0)
                        fi = j;
                    la = j;
                }
            }
            if(fi == 0) {
                for(int j = 1; j <= n; ++j) {
                    ++dr[j][1];
                    --dr[j][n + 1];
                }
            } else {
                int L = max(1, la - k + 1), R = fi;
                if(L <= R) {
                    for(int j = max(1, i - k + 1); j <= i; ++j) {
                        ++dr[j][L];
                        --dr[j][R + 1];
                    }
                }
            }
        }
        for(int j = 1; j <= n; ++j) {
            int fi = 0, la = 0;
            for(int i = 1; i <= n; ++i) {
                if(g[i][j] == 'B') {
                    if(fi == 0)
                        fi = i;
                    la = i;
                }
            }
            if(fi == 0) {
                for(int i = 1; i <= n; ++i) {
                    ++dc[1][i];
                    --dc[n + 1][i];
                }
            } else {
                int U = max(1, la - k + 1), D = fi;
                if(U <= D) {
                    for(int i = max(1, j - k + 1); i <= j; ++i) {
                        ++dc[U][i];
                        --dc[D + 1][i];
                    }
                }
            }
        }
     
        /*for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= n; ++j)
                printf("%d%c", dr[i][j], " 
    "[j == n]);
        for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= n; ++j)
                printf("%d%c", dc[i][j], " 
    "[j == n]);*/
     
        for(int i = 1; i <= n; ++i) {
            int cur = 0;
            for(int j = 1; j <= n + 1; ++j) {
                cur += dr[i][j];
                ans[i][j] += cur;
            }
        }
        for(int j = 1; j <= n; ++j) {
            int cur = 0;
            for(int i = 1; i <= n + 1; ++i) {
                cur += dc[i][j];
                ans[i][j] += cur;
            }
        }
        int res = 0;
        for(int i = 1; i <= n; ++i) {
            for(int j = 1; j <= n; ++j)
                res = max(res, ans[i][j]);
        }
        printf("%d
    ", res);
    }
    

    *E - Compress Words

    题意:给n个字符串,要求把他们依次首尾连接,每次连接可以把最长的重叠的前后缀重叠在一起,求结果。

    假算法:看起来可以二分每次重叠的长度,然后用哈希来验证,问题在于如何维护后缀哈希,更暴力就再用一个线段树。虽然看起来这个算法的复杂度是O(nlognlogn),但是仔细想一想并不是这样,首先线段树更新的时候,单点更新一个字符,最多更新n次,这部分复杂度是O(nlogn),然后在二分每次重叠的长度的时候,设新的字符串的长度是x,需要二分logx次,每次询问是logn,但是由于所有的x加起来才是n这么多,最坏的情况是一共添加n次长度为1的新字符串,复杂度也是O(nlogn),直观理解可以知道,新增的字符串平均长度越长,复杂度越低,因为log是个增长及其缓慢的函数。

    上面这个算法假在:最大的匹配长度并不是满足单调性的东西。

    题解:把上面的二分去掉,直接换成暴力。反正新增的每个字符最多被检测一次,也就是说,需要注意到匹配的过程和原串的长度并没有什么必然联系。再想想,好像连线段树也可以不要了。

    const int MAXN = 1000000;
    const int BASE = 2333;
    int BASEPOW[MAXN + 5];
    
    void Init() {
        BASEPOW[0] = 1;
        for(int i = 1; i <= MAXN; ++i)
            BASEPOW[i] = 1ll * BASEPOW[i - 1] * BASE % MOD;
    }
    
    char ans[MAXN + 5];
    char tmp[MAXN + 5];
    int anslen, tmplen;
    
    int ha1[MAXN + 5];
    int ha2[MAXN + 5];
    
    void test_case() {
        Init();
        int n;
        scanf("%d", &n);
        anslen = 0;
        while(n--) {
            scanf("%s", tmp + 1);
            tmplen = strlen(tmp + 1);
            int maxlen = 0, ceilen = min(anslen, tmplen);
            for(int x = 1; x <= ceilen; ++x) {
                ha1[x] = (1ll * ans[anslen - x + 1] * BASEPOW[x - 1] + ha1[x - 1]) % MOD;
                ha2[x] = (1ll * ha2[x - 1] * BASE + tmp[x]) % MOD;
                if(ha1[x] == ha2[x])
                    maxlen = x;
            }
            for(int i = 1 + maxlen; i <= tmplen; ++i)
                ans[++anslen] = tmp[i];
        }
        puts(ans + 1);
    }
    

    启示:哈希真是个好办法。

    深入理解KMP:其实这个匹配形式一开始确实觉得像KMP,但是不知道假如套KMP的话,谁是text串,谁是pattern串。事实上因为每次匹配的长度最大就是min(anslen,tmplen),截取ans串的这个长度的后缀放在后面,截取tmp串的这个长度的前缀放在前面,然后求一次前缀函数,得到的结果就是最长的重叠的真前后缀的长度。但是怎么保证结果不超过这个长度呢?

    比如:"ababa"+"babab"

    正确的答案应该是:"abab"

    但求出的最长重叠串是:"abababab"

    解决的办法是在中间加个'#'。

    变成:"ababa"+"#"+"babab"

    求出的最长重叠串是:"abab"

    #include<bits/stdc++.h>
    using namespace std;
    typedef unsigned int uint;
    typedef long long ll;
    typedef unsigned long long ull;
    typedef long double ld;
    typedef pair<int, int> pii;
    typedef pair<ll, ll> pll;
    
    const int INF = 1061109567;
    const ll LINF = 4557430888798830399ll;
    
    const int MOD = 1000000007;
    
    /*---*/
    
    const int MAXN = 1000000;
    
    int pi[2 * MAXN + 5];
    char s[2 * MAXN];
    
    void GetPrefixFunction(char *s, int sl) {
        pi[0] = 0, pi[1] = 0;
        for(int i = 1, k = 0; i < sl; ++i) {
            while(k && s[i] != s[k])
                k = pi[k];
            pi[i + 1] = (s[i] == s[k]) ? ++k : 0;
        }
    }
    
    char ans[MAXN + 5];
    char tmp[MAXN + 5];
    int anslen, tmplen;
    
    void test_case() {
        int n;
        scanf("%d", &n);
        anslen = 0;
        while(n--) {
            scanf("%s", tmp + 1);
            tmplen = strlen(tmp + 1);
            int ceilen = min(anslen, tmplen), sl = 0;
            for(int x = 1; x <= ceilen; ++x)
                s[sl++] = tmp[x];
            //s[sl++] = '#';
            for(int x = 1; x <= ceilen; ++x)
                s[sl++] = ans[anslen - ceilen + x];
            GetPrefixFunction(s, sl);
            int maxlen = min(ceilen, pi[sl]);
            for(int i = 1 + maxlen; i <= tmplen; ++i)
                ans[++anslen] = tmp[i];
        }
        puts(ans + 1);
    }
    
    int main() {
    #ifdef KisekiPurin
        freopen("KisekiPurin.in", "r", stdin);
    #endif // KisekiPurin
        int t = 1;
        //scanf("%d", &t);
        for(int i = 1; i <= t; ++i) {
            //printf("Case #%d:", i);
            test_case();
        }
        return 0;
    }
    

    *F - Graph Traveler

    题目:有n(<=1000)个点,每个点有[1,10]条出边,且每个点有个权值,从某个点出发的时候会加上当前点的权值,然后根据权值mod当前点的出边数量,选择那条唯一的出边走出去。很显然无论从哪个点以什么初始权值出发,都会无限走下去。若干次询问,每次询问,回答某个起点以某个初始权值出发,无穷次经过的点的数量。

    这个东西并不等价于问题“给一片1000*10个点的内向基环树森林,每个点记录当前的总权值,对每一个起点确定从它出发可以无穷次经过的点的数量。”,因为每个点的出边数量是不一定的,所以不能在某一个点模出边数量。遂看题解。

    题解:虽然不能在某一个点模出边数量,但是因为[1,10]的lcm是2520,所以真正有用的权值是[0,2520),构造一片1000*2520个点的内向基环树森林,dp即可。需要注意很多个细节,比如转移的时候是要加上终点的权值,而不是起点的权值。最好写个小的LCM=12观察样例,然后写几个id和反id观察是不是搞错了。

    基环树计算环的大小,本身是不需要SET的,但是这里要记录原本的节点的编号,再去重,假如用不排序去重的方法有可能导致复杂度错误。

    const int LCM = 2520;
    const int MAXN = 1000;
    const int MAXNLCM = MAXN * LCM;
    int K[MAXN + 5];
    int G[MAXNLCM + 5];
    
    int id(int i, int j) {
        return (i - 1) * LCM + j + 1;
    }
    
    int id_i(int id) {
        return (id - 1) / LCM + 1;
    }
    
    int id_j(int id) {
        return (id - 1) % LCM;
    }
    
    int C[MAXNLCM + 5], cntC;
    int DP[MAXNLCM + 5];
    
    set<int> SET;
    stack<int> STACK;
    
    int inC;
    int dfs(int u, int c) {
        if(C[u]) {
            if(C[u] == c) {
                inC = u;
                return 0;
            }
            return DP[u];
        }
        C[u] = c;
        if(DP[u] = dfs(G[u], c))
            return DP[u];
        if(inC) {
            SET.insert(id_i(u));
            STACK.push(u);
            if(u == inC) {
                int siz = SET.size();
                while(!STACK.empty()) {
                    int TOP = STACK.top();
                    STACK.pop();
                    DP[TOP] = siz;
                }
                inC = 0;
                SET.clear();
                return DP[u];
            }
            return 0;
        }
        exit(-1);
    }
    
    
    void test_case() {
        int n;
        scanf("%d", &n);
        int e[10];
        for(int i = 1, k; i <= n; ++i) {
            scanf("%d", &k);
            k = (k % LCM + LCM) % LCM;
            K[i] = k;
        }
        for(int i = 1, m; i <= n; ++i) {
            scanf("%d", &m);
            for(int mi = 0; mi < m; ++mi)
                scanf("%d", &e[mi]);
            for(int j = 0; j < LCM; ++j) {
                int vi = e[j % m];
                int vj = (j + K[vi]) % LCM;
                G[id(i, j)] = id(vi, vj);
            }
        }
    
        cntC = 0;
        for(int i = 1; i <= LCM * n; ++i) {
            if(C[i] == 0)
                dfs(i, ++cntC);
        }
    
        int q;
        scanf("%d", &q);
        while(q--) {
            int i, j;
            scanf("%d%d", &i, &j);
            j = (j % LCM + LCM) % LCM;
            j = (j + K[i]) % LCM;
            printf("%d
    ", DP[id(i, j)]);
        }
    }
    

    总结:基环树复杂版?基环树计算环的大小的方法大概就是这样了,若遇到环入口或者dfs返回值非0或者遇到其他dfs的染色块,则说明已经退出环,则计算栈中的环的信息,或者直接继承DP值。其他情况就是非环入口的环节点,返回值为0。注意在基环树中开始dfs并不能只从入度为0的点开始,有可能整个基环树就是一个环。但是从任意一个点开始是可以的,因为假如在环中,则这个就是环入口,会把整个环染色。否则一定会进入某个环入口,然后把整个环染色,无论如何都会把这个点能到达的环染色。

    假如换成vector排序去重,好像使用了更多空间,不过节省了时间,道理是显然的,因为set是最多拥有1000个数,而vector是非常浪费空间的。有个办法是记录一个虚拟的vector大小,当大小超过某个极限时把vector中的元素插进set,然后改用set统计,这样貌似还不如直接用set统计,反正复杂度是对的。

    基环树是否可以写成一个非递归的版本?

  • 相关阅读:
    我是来讲笑话的
    dom4j读取xml
    Mysql常用命令
    如何快速开发小型系统
    Spring aop的实现原理
    Spring IOC容器解析及实现原理
    如何编写更棒的代码
    Git使用教程
    关于程序员吃青春饭问题之探讨
    如何自学编程
  • 原文地址:https://www.cnblogs.com/KisekiPurin2019/p/12445075.html
Copyright © 2011-2022 走看看