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], " \n"[j == n]);
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= n; ++j)
            printf("%d%c", dc[i][j], " \n"[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\n", 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\n", DP[id(i, j)]);
    }
}

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

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

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

posted @ 2020-03-08 21:33  KisekiPurin2019  阅读(142)  评论(0编辑  收藏  举报