Codeforces Round #606 (Div. 2, based on Technocup 2020 Elimination Round 4)

A - Happy Birthday, Polycarp!

题意:给一个n<=1e9,求[1,n]里面有多少个数字是有单个数字重复多次构成的。

题解:这种数字本身不对,一个一个暴力验证就可以。假如数据量继续扩大,那么把所有的数字生成出来(至多200个,当1e18时),排个序,然后把n在里面二分,应该速度会大概提升10倍。

void test_case() {
    int n, ans = 0;
    scanf("%d", &n);
    for(int x = 1; x <= 9; ++x) {
        ll tmp = x;
        while(tmp <= n) {
            ++ans;
            tmp = 10 * tmp + x;
        }
    }
    printf("%d\n", ans);
}

B - Make Them Odd

题意:给一个序列,你每次可以选一个偶数,然后把所有等于这个偶数的数变成原来的一半,求最小的操作步数。

题解:每次取最大的出来贪心,每个数最多被贪30次,再加上优先队列的log就不太美妙了,但是总的复杂度依然是nlogn+nlogai。经过观察发现能够连在一起的都是非2的部分一样的数,也就是每种除去2的幂之后一样的数贡献这一种的最高2的幂,还是要用map去统计,复杂度没变。

附带一个更快的分解2的幂次的办法:__builtin_ctz(x),返回后跟0的数量。来源:https://www.cnblogs.com/liuzhanshan/p/6861596.html

int a[200005];
map<int, int> m;
void test_case() {
    int n, ans = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        int tmp = __builtin_ctz(a[i]);
        m[a[i] >> tmp] = max(m[a[i] >> tmp], tmp);
    }
    for(auto &i : m)
        ans += i.second;
    m.clear();
    printf("%d\n", ans);
}

C - As Simple as One and Two

题意:给一个字符串,移除最小数量的字符使得里面既没有子串"one"也没有子串"two"。

题解:一开始在考虑一个dp[04][j]表示前j个字符经过最少dp[04][j]次移除操作之后停留目前停留在状态0~4的做法,下次把这个补上。事实上想到一种更好的思路,首先重叠的字符串形如"xtwoney",这时候拿走"o"就一箭双雕,其他的情况形如"xtwoy"和"xoney"都是拿走中间的那个字符,因为拿走两边的可能x和y也会连上来。易知这就是最小构造。

char s[2000005];
vector<int>ans;
void test_case() {
    scanf("%s", s + 1);
    int n = strlen(s + 1);
    ans.clear();
    for(int i = 3; i <= n; ++i) {
        if(s[i - 2] == 't' && s[i - 1] == 'w' && s[i] == 'o') {
            if(i + 1 <= n && s[i + 1] == 'o') {
                s[i - 1] = '*';
                ans.push_back(i - 1);
            } else {
                s[i] = '*';
                ans.push_back(i);
            }
        }
    }
    for(int i = 3; i <= n; ++i) {
        if(s[i - 2] == 'o' && s[i - 1] == 'n' && s[i] == 'e') {
            s[i - 1] = '*';
            ans.push_back(i - 1);
        }
    }
    printf("%d\n", (int)ans.size());
    for(auto &v : ans)
        printf("%d ", v);
    printf("\n");
}

D - Let's Play the Words?

题意:给一堆01串玩成语接龙,要求翻转最少的串使得把所有的串都用在一起。

题解:和上次那个奇偶填数的类似,应该就是01串和10串的数量差<=1就可以,而且假如00和11同时存在则至少要有一个01或者10作为黏着剂。这里要求翻转串之后也没有相同的串,所以有反串的就直接不允许翻转,用正数表示翻转此串会导致01->10,用负数表示翻转此串会导致10->01,0表示它的反串存在。若反串检测到0那么就把正串也标为0。注意这种hash的时候要随着串的移动添加一个常数在后面区别"1","01"和"001"。

用hash+map的实现,390ms。
用hash+unorder_map的实现,243ms。

不知道从哪里掏出来的一个图:来源

由上图可知,假如选的数字随机分布,那么中奖的概率比买彩票中奖的概率还要小。

unordered_map<ull, int> m;
vector<int> v;
int l;
char s[4000005];
ull toKey1() {
    ull key1 = 0;
    for(int i = 1; i <= l; ++i)
        key1 = key1 * 23333 + (s[i] - '0' + 23);
    return key1;
}
 
ull toKey2() {
    ull key2 = 0;
    for(int i = l; i >= 1; --i)
        key2 = key2 * 23333 + (s[i] - '0' + 23);
    return key2;
}
 
void test_case() {
    int n;
    scanf("%d", &n);
    m.clear();
    int cnt01 = 0, cnt10 = 0;
    bool cnt11 = 0, cnt00 = 0;
    for(int i = 1; i <= n; ++i) {
        scanf("%s", s + 1);
        l = strlen(s + 1);
        if(s[1] == s[l]) {
            if(s[1] == '0')
                cnt00 = 1;
            else
                cnt11 = 1;
            continue;
        }
        ull k1 = toKey1();
        ull k2 = toKey2();
        auto it = m.find(k1);
        if(it == m.end()) {
            if(s[1] == '0') {
                m[k1] = i;
                m[k2] = 0;
                ++cnt01;
            } else {
                m[k1] = -i;
                m[k2] = 0;
                ++cnt10;
            }
        } else {
            m[k1] = 0;
            m[k2] = 0;
            if(s[1] == '0')
                ++cnt01;
            else
                ++cnt10;
        }
    }
    if(cnt00 && cnt11) {
        if(cnt01 + cnt10 == 0) {
            puts("-1");
            return;
        }
    }
    if(abs(cnt01 - cnt10) <= 1) {
        puts("0");
        puts("");
        return;
    }
    v.clear();
    if(cnt01 > cnt10) {
        for(auto &i : m) {
            if(i.second > 0)
                v.push_back(i.second);
        }
        int rest = (cnt01 - cnt10) / 2;
        if(rest > v.size()) {
            puts("-1");
            return;
        }
        printf("%d\n", rest);
        for(int i = 0; i < rest; ++i)
            printf("%d ", v[i]);
        puts("");
        return;
    } else {
        for(auto &i : m) {
            if(i.second < 0)
                v.push_back(-i.second);
        }
        int rest = (cnt10 - cnt01) / 2;
        if(rest > v.size()) {
            puts("-1");
            return;
        }
        printf("%d\n", rest);
        for(int i = 0; i < rest; ++i)
            printf("%d ", v[i]);
        puts("");
        return;
    }
}

假如害怕冲突,可以使用复杂度更低的Trie的实现。

vector<int> v;

struct TrieNode {
    int data;
    int nxt[2];

    void Init() {
        data = 0;
        memset(nxt, 0, sizeof(nxt));
    }
};

struct Trie {
    static const int MAXN = 4000000;
    TrieNode tn[MAXN + 5];
    int root, top;

    int NewNode() {
        tn[++top].Init();
        return top;
    }

    void Init() {
        top = 0;
        root = NewNode();
    }

    void Insert(char *a, int len, int data) {
        int cur = root;
        for(int i = 1; i <= len; ++i) {
            int &nxt = tn[cur].nxt[a[i] - '0'];
            if(!nxt)
                nxt = NewNode();
            cur = nxt;
        }
        tn[cur].data = data;
    }

    int Query(char *a, int len) {
        int cur = root;
        for(int i = 1; i <= len; ++i) {
            int &nxt = tn[cur].nxt[a[i] - '0'];
            if(!nxt)
                return 0;
            cur = nxt;
        }
        return tn[cur].data;
    }

    void _dfs(int cur, int val) {
        if(tn[cur].data && tn[cur].data != INF) {
            if(val == 1 && tn[cur].data >= 1)
                v.push_back(tn[cur].data);
            if(val == -1 && tn[cur].data <= -1)
                v.push_back(-tn[cur].data);
        }
        if(tn[cur].nxt[0])
            _dfs(tn[cur].nxt[0], val);
        if(tn[cur].nxt[1])
            _dfs(tn[cur].nxt[1], val);
    }

    void dfs(int val) {
        _dfs(root, val);
    }
} trie;

int l;
char s[4000005];

void test_case() {
    int n;
    scanf("%d", &n);
    trie.Init();
    int cnt01 = 0, cnt10 = 0;
    bool cnt11 = 0, cnt00 = 0;
    for(int i = 1; i <= n; ++i) {
        scanf("%s", s + 1);
        l = strlen(s + 1);
        if(s[1] == s[l]) {
            if(s[1] == '0')
                cnt00 = 1;
            else
                cnt11 = 1;
            continue;
        }
        int res = trie.Query(s, l);
        if(res == 0) {
            if(s[1] == '0') {
                ++cnt01;
                trie.Insert(s, l, i);
            } else {
                ++cnt10;
                trie.Insert(s, l, -i);
            }
        } else {
            if(s[1] == '0')
                ++cnt01;
            else
                ++cnt10;
        }
        reverse(s + 1, s + 1 + l);
        trie.Insert(s, l, INF);
    }
    if(cnt00 && cnt11) {
        if(cnt01 + cnt10 == 0) {
            puts("-1");
            return;
        }
    }
    if(abs(cnt01 - cnt10) <= 1) {
        puts("0");
        puts("");
        return;
    }
    v.clear();
    if(cnt01 > cnt10) {
        trie.dfs(1);
        int rest = (cnt01 - cnt10) / 2;
        if(rest > v.size()) {
            puts("-1");
            return;
        }
        printf("%d\n", rest);
        for(int i = 0; i < rest; ++i)
            printf("%d ", v[i]);
        puts("");
        return;
    } else {
        trie.dfs(-1);
        int rest = (cnt10 - cnt01) / 2;
        if(rest > v.size()) {
            puts("-1");
            return;
        }
        printf("%d\n", rest);
        for(int i = 0; i < rest; ++i)
            printf("%d ", v[i]);
        puts("");
        return;
    }
}

使用Trie的时候注意区分找不到字符串(返回0),和反串被使用(返回INF)。

E - Two Fairs

题意:给一个无向连通图,给定x和y两个点,求有多少个不含x也不含y的无序对(u,v),满足u到v的任何一条路都经过x且经过y。

题解:假如改成最短路又变成了考Dijkstra松弛然后找最短路径生成树再加上最短路径生成树外的附加边?不过这里我使用的是进行一次求割点,当x和y都是割点时,他们之间夹的点以及他们本身去掉,剩下的两个部分相乘就是答案。求割点使用Tarjan算法。一开始还在搞什么双连通分量缩点,其实真的没必要而且也没关系。注意其实求割点的算法是为了一次求出一群割点,假如只是验证某个点是不是割点的话,就把这个点堵住,从任意一个点出发开始dfs,假如有点不能被遍历到则堵住了割点(连通图)。然后往返搞两次,被两次都经过的点就是两边割点中间公共的那部分,直接减去就可以了。所以正解是两次堵点搜索。

还是总结一下我的算法,虽然蠢但它是对的。验证两个都是割点之后,堵住y,从x开始搜索vis2,记录所有能够到y的点的点集V(注意是点集!WA了几次了!),然后堵住x和y从这些点集搜索vis3,得到的就是中间部分。剩下的也是堵住x,然后从x开始走vis3,走到的就是集合X,同理整出Y。

struct edge {
    int nxt, mark;
} pre[4000010];
int n, m, idx, cnt, tot;
int head[2000010], DFN[2000010], LOW[2000010];
bool cut[2000010];
 
void add(int x, int y) {
    pre[++cnt].nxt = y;
    pre[cnt].mark = head[x];
    head[x] = cnt;
}
 
void tarjan(int u, int fa) {
    DFN[u] = LOW[u] = ++idx;
    int child = 0;
    for(int i = head[u]; i != 0; i = pre[i].mark) {
        int nx = pre[i].nxt;
        if(!DFN[nx]) {
            tarjan(nx, fa);
            LOW[u] = min(LOW[u], LOW[nx]);
            if(LOW[nx] >= DFN[u] && u != fa)
                cut[u] = 1;
            if(u == fa)
                child++;
        }
        LOW[u] = min(LOW[u], DFN[nx]);
    }
    if(child >= 2 && u == fa)
        cut[u] = 1;
}
 
bool vis2[2000010];
bool vis3[2000010];
vector<int> fi;
int x, y;
void dfs2(int u) {
    if(vis2[u])
        return;
    vis2[u] = 1;
    for(int i = head[u]; i != 0; i = pre[i].mark) {
        int nx = pre[i].nxt;
        if(vis2[nx])
            continue;
        if(nx == y) {
            fi.push_back(u);
            continue;
        }
        dfs2(nx);
    }
}
 
int cnta = 0;
void dfs3(int u) {
    vis3[u] = 1;
    ++cnta;
    for(int i = head[u]; i != 0; i = pre[i].mark) {
        int nx = pre[i].nxt;
        if(vis3[nx])
            continue;
        dfs3(nx);
    }
}
 
int cntb = 0;
void dfs4(int u) {
    vis3[u] = 1;
    ++cntb;
    for(int i = head[u]; i != 0; i = pre[i].mark) {
        int nx = pre[i].nxt;
        if(vis3[nx])
            continue;
        dfs4(nx);
    }
}
 
void dfs5(int u) {
    vis3[u] = 1;
    for(int i = head[u]; i != 0; i = pre[i].mark) {
        int nx = pre[i].nxt;
        if(vis3[nx])
            continue;
        dfs5(nx);
    }
}
 
void test_case() {
    scanf("%d%d%d%d", &n, &m, &x, &y);
    memset(DFN, 0, sizeof(DFN[0]) * (n + 1));
    memset(LOW, 0, sizeof(LOW[0]) * (n + 1));
    memset(head, 0, sizeof(head[0]) * (n + 1));
    memset(cut, 0, sizeof(cut[0]) * (n + 1));
    memset(vis2, 0, sizeof(vis2[0]) * (n + 1));
    memset(vis3, 0, sizeof(vis3[0]) * (n + 1));
    idx = 0, cnt = 0, tot = 0;
    for(int i = 0; i <= 2 * m + 10; ++i) {
        pre[i].nxt = pre[i].mark = 0;
    }
    cnt = 0;
    for(int i = 1; i <= m; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        add(b, a);
    }
    for(int i = 1; i <= n; i++) {
        if(DFN[i] == 0)
            tarjan(i, i);
        //cout << "cut[" << i << "]=" << cut[i] << endl;
    }
    if(!cut[x] || !cut[y]) {
        puts("0");
        return;
    } else {
        fi.clear();
        dfs2(x);
        //找y向x的第一个点。
        //printf("%d\n", fi);
 
        vis3[x] = 1;
        vis3[y] = 1;
        for(auto &v : fi) {
            if(!vis3[v])
                dfs5(v);
        }
 
        /*for(int i = 1; i <= n; i++)
            cout << "vis[" << i << "]=" << vis3[i] << endl;*/
 
        cnta = 0;
        for(int i = head[x]; i != 0; i = pre[i].mark) {
            int nx = pre[i].nxt;
            if(vis3[nx])
                continue;
            dfs3(nx);
        }
        cntb = 0;
        for(int i = head[y]; i != 0; i = pre[i].mark) {
            int nx = pre[i].nxt;
            if(vis3[nx])
                continue;
            dfs4(nx);
        }
        printf("%lld\n", 1ll * cnta * cntb);
        return;
    }
}
posted @ 2019-12-15 12:46  KisekiPurin2019  阅读(282)  评论(0编辑  收藏  举报