Codeforces Round #594 (Div. 2)

A - Integer Points

题意:给n条斜率为1和m条斜率为-1的直线,求交点在整点的个数。

题解:观察下面给的图,发现截距相差为偶数的点才有交点在整点。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
 
int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t;
    scanf("%d", &t);
    while(t--) {
        int n;
        scanf("%d", &n);
        int p[2] = {}, q[2] = {};
        while(n--) {
            int pi;
            scanf("%d", &pi);
            ++p[pi & 1];
        }
        int m;
        scanf("%d", &m);
        while(m--) {
            int qi;
            scanf("%d", &qi);
            ++q[qi & 1];
        }
        printf("%lld
", 1ll * p[0]*q[0] + 1ll * p[1]*q[1]);
    }
}

B - Grow The Tree

题意:有n根木棍,要从原点开始横竖横竖间隔摆,求离原点最远距离。

题解:很显然不要求间隔摆就直接一根直接插出去,所以可以试试贪心,用最短的一半下整做竖,其他做横。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int a[100005];

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int n;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    sort(a + 1, a + 1 + n);
    ll sum1 = 0;
    for(int i = 1, c = n / 2; i <= c; ++i)
        sum1 += a[i];
    ll sum2 = 0;
    for(int i = n / 2 + 1; i <= n; ++i)
        sum2 += a[i];
    printf("%lld
", sum1 * sum1 + sum2 * sum2);
}

C - Ivan the Fool and the Probability Theory

题意:给1e5*1e5级别的棋盘格,称棋盘格“随机”当且仅当它每个格子至多有一个相邻的格子与他同色。

不会写,数据量略大。

补题题解:首先考虑只有1行的情况,非常简单,随便dp一下。

然后有一个引理:当确定第1行之后,后面的行只能和相邻行完全相同或者完全相反。证明:若结论不成立,至少存在一个位置 (j) ,使得 (a_{2,j})(a_{1,j}) 相同,而 (a_{2,j+1})(a_{1,j+1}) 相反,那么:当 (a_{1,j})(a_{1,j+1}) 同色,则 (a_{1,j}) 有两个相邻同色;当 (a_{1,j})(a_{1,j+1}) 相反,则 (a_{2,j}) 有两个相邻同色。且显然的,假如第1行出现了至少1个连续的2格组,那么以后都不能选完全相同,只能选完全相反,对于这种情况只需要确定第一行就可以全部统计完毕。另一种情况是,只有完全间隔的格子可以选择复制一行,且下一次必须选择相反复制,那这种情况压缩到一列去观察,发现和这个问题的1*m版本是一样的(但是不能够乘2,以为原本求出的dp数组就已经是包含两种颜色的)。

所以就是 ((dp[m]-2)+dp[n])

启发:不会做的话是不是可以dp出一行或者2行的结果看看规律,然后手画几个小样例看看能不能套上去吧。不过下一次应该是学会这样分析了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MOD = 1e9 + 7;
int dp[100005][2];

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int n, m;
    while(~scanf("%d%d", &n, &m)) {
        if(n > m)
            swap(n, m);
        dp[1][0] = 2;
        for(int i = 2; i <= m; ++i) {
            dp[i][0] = (dp[i - 1][1] + dp[i - 1][0]) % MOD;
            dp[i][1] = dp[i - 1][0];
        }
        printf("%lld
", ((1ll * dp[m][0] + dp[m][1] - 2ll) % MOD + (1ll * dp[n][0] + dp[n][1]) % MOD) % MOD);
    }
}

D1 - The World Is Just a Programming Task (Easy Version)

见下题

D2 - The World Is Just a Programming Task (Hard Version)

题意:给一个括号串,可以调换其中的任意两个括号的位置(可以自己换自己),使得新括号串的循环移位构成的合法串最多。多方案任输出其中之一。

题解:画一下发现,合法括号串,必须是从0开始的不越过0分界线的到0结束的一段折线。那么循环移位的数量就相当于触碰0点的数量。因为是循环移位所以不妨找出最早的一个最低点作为0点。假如有两个以上0点,那么改变其中之一不会变得更好,此时把0点前的一个非0点的顺序换一下有可能会导致答案+1。

所以口胡出一个做法:先检查是否左右括号平衡,不平衡直接 0 1 1 ,然后检测是否已经满了(都是两个括号间隔着),是则输出 n/2 1 1 ,否则找到零点的数量,若只有一个,则尝试把零点破坏。然后都尝试一下在连续下坡两次的零点前构造一个新的,比比谁多。

但是很难写。我决定看别人怎么做。

补题题解:发现别人和我的分析是一样的,不过别人直接指出了,把一个左括号和一个右括号互换,会使得其中夹着的所有的点的y均-2。所以问题转化成了,对于某个高度为1的左括号,一直向右扩展到第一个高度为1的右括号,可以尝试把中间遇到的所有y=2的点翻折下来,而在这个右括号之后假如跟着左括号(回到2)则可以继续,否则break掉。可以发现这就是被零点打断的一些区间,最好的做法就是在交换两个零点之间的括号。复杂度O(n)。

具体来说,假如编号为0,1,2,3,...,n号点,其中n号和0号是同一个点且他们都是零点,设相邻的某两个零点x1,x2,若x2-x1>2,则尝试交换x1+2,和x2-1。贡献为[x1,x2]中y=2的点的数量。问题转化成怎么找循环位移的零点。这个就不多说了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n;
char *s, t[600005];

int gans, gai, gaj;

void solve1() {
    //把2翻转成0
    int ans1 = 0, ans2 = 0, ai = 1, aj = 1;
    int cnt0 = 0, cnt2 = 0, lastp = -1;
    for(int i = 1; i <= n; ++i) {
        if(s[i] == '(') {
            ++cnt0;
            if(cnt0 == 1)
                lastp = i;
            else if(cnt0 == 2)
                ++cnt2;
        } else {
            --cnt0;
            if(cnt0 == 0)
                ++ans1;
            else if(cnt0 == 1) {
                if(cnt2 > ans2) {
                    ans2 = cnt2;
                    ai = lastp + 1;
                    aj = i;
                }
                lastp = i;
                cnt2 = 0;
            } else if(cnt0 == 2)
                ++cnt2;
        }
    }
    gans = ans1 + ans2;
    gai = ai, gaj = aj;
}

void solve2() {
    //把1翻转为-1
    int ans = 0, ai = 1, aj = 1;
    int cnt0 = 0, cnt1 = 0, lastp = 0;
    for(int i = 1; i <= n; ++i) {
        if(s[i] == '(') {
            ++cnt0;
            if(cnt0 == 1)
                ++cnt1;
        } else {
            --cnt0;
            if(cnt0 == 0) {
                if(cnt1 > ans) {
                    ans = cnt1;
                    ai = lastp + 1;
                    aj = i;
                }
                lastp = i;
                cnt1 = 0;
            } else if(cnt0 == 1)
                ++cnt1;
        }
    }
    if(ans > gans) {
        gans = ans;
        gai = ai;
        gaj = aj;
    }
    int p = s - t;
    gai += p;
    gaj += p;
    if(gai > n)
        gai -= n;
    if(gaj > n)
        gaj -= n;
    printf("%d
%d %d
", gans, gai, gaj);
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    while(~scanf("%d%s", &n, t + 1)) {
        if(n & 1) {
            printf("0
1 1
");
            continue;
        }
        int cnt = 0, mincnt = 1e9;
        for(int i = 1; i <= n; ++i) {
            t[i + n] = t[i];
            if(t[i] == '(')
                ++cnt;
            else {
                --cnt;
                if(cnt < mincnt) {
                    mincnt = cnt;
                    s = &t[i];
                }
            }
        }
        if(cnt) {
            printf("0
1 1
");
            continue;
        }
        s[n + 1] = '';
        solve1();
        solve2();
    }
    return 0;
}

E - Queue in the Train

题意:有n个人坐火车,第i个人会在ti时刻开始想去泡面,每个人花的时间都是p。泡泡面是要排队的,但是大家都不喜欢排队,每个人去排队时,队列中不可以有编号比他小的人(意思是可以有编号比他大的),若多个人同时想去,则神奇的只有编号最小的会去排队。

错误尝试:一开始直接按时间排序然后丢进堆里面按编号弹出来,这个错误理解题意,不过吸收了一下这个对时间扫描的写法(以前貌似不会写,现在码力确实上升了)。应该是直接模拟,对时间排序,然后同一个时刻的按编号顺序排。这样每个人丢进堆里面。这样堆按编号小优先的话,同时想去的这堆人就只能是堆顶了。因为堆顶假如不能去则整个堆都不能去,这是显然的。

所以就再整一个队列。每次查一个位置前面的人是不是有人在队列中的话,用树状数组就可以(求前缀和,貌似从一个二分找奇数偶数前缀和的题目得到启发)。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n, p;
pair<int, int> t[100005];
priority_queue<int, vector<int>, greater<int> >pq;
queue<int> q;
ll ans[100005];

int bit[100005];

void add(int x, int v) {
    for(; x <= n; x += x & -x)
        bit[x] += v;
}

int sum(int x) {
    int res = 0;
    for(; x; x -= x & -x)
        res += bit[x];
    return res;
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    scanf("%d%d", &n, &p);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &t[i].first);
        t[i].second = i;
    }
    sort(t + 1, t + 1 + n);
    ll curp = 0;
    int i = 1, cnt = 0;
    while(cnt < n) {
        if(q.empty() && pq.empty() && i <= n && curp < t[i].first)
            curp = t[i].first;
        if(q.size()) {
            int u = q.front();
            curp += p;
            ans[u] = curp;
            add(u, -1);
            q.pop();
            ++cnt;
        }
        while(i <= n && t[i].first <= curp) {
            pq.push(t[i].second);
            ++i;
        }
        while(pq.size()) {
            int u = pq.top();
            if(sum(u) == 0) {
                q.push(u);
                add(u, 1);
                pq.pop();
            } else
                break;
        }
    }
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", ans[i], " 
"[i == n]);

    return 0;
}

使用这个结构貌似容易颠倒一些事件的顺序,正确的做法是把所有的事件写成结构体然后排序(类似cdq分治),有几种事件:1、有人开始想要泡面,2、有人进入了队伍里排队泡面,3、有人泡完面结束了。

然后发现事件2是可以立刻处理的,就不需要进入优先队列里面了。学习了别人对时间轴的处理方法。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n, p;
struct Event {
    ll t, op, id;
    //时间、事件类型、序号
    Event(ll _t, int _op, int _id) {
        t = _t, op = _op, id = _id;
    }
    bool operator<(const Event &e)const {
        return t != e.t ? t > e.t : (op != e.op ? op > e.op : id > e.id);
    }
};
priority_queue<Event> _time;
set<int> inq, wnt;

ll ans[100005];

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    scanf("%d%d", &n, &p);
    for(int i = 1, ti; i <= n; ++i) {
        scanf("%d", &ti);
        _time.push(Event(ti, 1, i));
    }
    ll curtime = 0;
    while(!_time.empty()) {
        Event e = _time.top();
        _time.pop();
        if(e.op == 1) {
            if(inq.empty() || *inq.begin() > e.id) {
                curtime = max(curtime, e.t);
                curtime += p;
                _time.push(Event(curtime, 3, e.id));
                inq.insert(e.id);
            } else
                wnt.insert(e.id);
        } else {
            ans[e.id] = e.t;
            inq.erase(e.id);
            if(!wnt.empty() && (inq.empty() || *inq.begin() > *wnt.begin())) {
                auto tmp = wnt.begin();
                curtime += p;
                _time.push(Event(curtime, 3, *tmp));
                inq.insert(*tmp);
                wnt.erase(tmp);
            }
        }
    }
    for(int i = 1; i <= n; ++i)
        printf("%lld%c", ans[i], " 
"[i == n]);

    return 0;
}

总结1:对这种时间轴,分析得到其实在curtime入队inq之后的下一个有用的1事件必定推迟到curtime+p时发生。而3事件是直接交给时间轴处理的。大概的意思是用优先队列维护一整个时间轴,每次从时间轴的顶部取出一些事件,由于这个题目的约定我们规定事件3早于事件1发生。还是得慢慢领悟。

总结2:时间轴的事件是不随curtime变化的,这个是priority_queue实现的一个优点,curtime的作用只是给事件的发生时间打好标记(就是入队inq了之后,它完成的时间就是curtime+=p。取出一个事件的时候,若curtime超过t,那么t会推迟到curtime执行,否则是curtime浪费时间空等到t,所以取max(然后再+=p)。也就是curtime记录的只是当前队列的队尾元素完成的时间,而不是当前时间,假如换个名字叫做queue_time会更合适。),而不管时间轴是怎么运作的。总之就是码力不足,知道自己的写法为什么错,但是不知道什么是正确的写法。

F - Catowice City

题意:有n个人,选其中j个做裁判,剩下n-j个做参赛选手,要求任意一种合法的分配。合法的分配是指:每个裁判都不认识每个参赛选手。(注意认识关系是单向的,自己必定认识自己)

题解:看起来就像是一个“传递闭包”,认识关系连一条有向边,然后选择一个点必须选择它的整个闭包作为裁判,假如还有点没有选中很明显就可以全部作为参赛选手。问题变成找一个点,不能出发到达所有的点。

补题题解:图论是不太会的,直接看别人的参考。有一种方法是求出点强连通分量,那么一个点强连通分量一定是循环选择的,然后强连通的这个分量缩成一个点。假如缩点之后图中只有一个点,那么就No,否则缩点之后必定无环,选择其中一个出度为0的点作为裁判,剩下的作为选手,因为选手可以认识裁判,裁判不能认识选手。

封装了一个Kosaraju的模板,通过类似网络流的使用方法来添加,实现方式为vector(比前向星略微方便),输出的n2,G2就是缩点后新图的节点数和图的样子,V就是新图每个节点对应的强连通分量,而查询某个点所属的强连通分量则用c2。为了不过多使用::符,在Kosaraju中写一个solve处理新图就可以了。或者using namespace SCC也可以。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

namespace SCC {
    const int MAXN = 1e6 + 5;

    int n;
    vector<int> G[MAXN], BG[MAXN];

    int c1[MAXN], cntc1;
    int c2[MAXN], cntc2;
    int s[MAXN], cnts;

    int n2;
    vector<int> V[MAXN];
    vector<int> G2[MAXN], BG2[MAXN];

    void init(int _n) {
        n = _n;
        cntc1 = 0, cntc2 = 0, cnts = 0;
        for(int i = 1; i <= n; ++i) {
            G[i].clear();
            BG[i].clear();
            c1[i] = 0;
            c2[i] = 0;
            s[i] = 0;
            V[i].clear();
            G2[i].clear();
            BG2[i].clear();
        }
    }

    void add_edge(int u, int v) {
        G[u].push_back(v);
        BG[v].push_back(u);
    }

    void dfs1(int u) {
        c1[u] = cntc1;
        for(int v : G[u]) {
            if(!c1[v])
                dfs1(v);
        }
        s[++cnts] = u;
    }

    void dfs2(int u) {
        V[cntc2].push_back(u);
        c2[u] = cntc2;
        for(int v : BG[u]) {
            if(!c2[v])
                dfs2(v);
        }
    }

    void Kosaraju() {
        for(int i = 1; i <= n; ++i) {
            if(!c1[i]) {
                ++cntc1;
                dfs1(i);
            }
        }

        for(int i = n; i >= 1; --i) {
            if(!c2[s[i]]) {
                ++cntc2;
                dfs2(s[i]);
            }
        }

        n2 = cntc2;
        for(int i = 1; i <= n2; ++i) {
            for(auto u : V[i]) {
                for(auto v : G[u]) {
                    if(c2[v] != i) {
                        G2[i].push_back(c2[v]);
                        BG2[c2[v]].push_back(i);
                    }
                }
            }
        }

        for(int i = 1; i <= n2; ++i) {
            sort(G2[i].begin(), G2[i].end());
            G2[i].erase(unique(G2[i].begin(), G2[i].end()), G2[i].end());
            sort(BG2[i].begin(), BG2[i].end());
            BG2[i].erase(unique(BG2[i].begin(), BG2[i].end()), BG2[i].end());
        }
    }

    void solve() {
        int id = -1;
        for(int i = 1; i <= n2; ++i) {
            if(G2[i].size() == 0) {
                id = i;
                break;
            }
        }
        printf("%d %d
", V[id].size(), n - V[id].size());
        for(int i = 0, siz = V[id].size(); i < siz; ++i)
            printf("%d%c", V[id][i], " 
"[i == siz - 1]);
        int fi = 1;
        for(int i = 1; i <= n2; ++i) {
            if(i != id) {
                for(int j = 0, siz = V[i].size(); j < siz; ++j) {
                    if(fi) {
                        printf("%d", V[i][j]);
                        fi = 0;
                    } else
                        printf(" %d", V[i][j]);
                }
            }
        }
        printf("
");
    }
}

int main() {
#ifdef KisekiPurin
    freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
    int t;
    scanf("%d", &t);
    while(t--) {
        int n, m;
        scanf("%d%d", &n, &m);

        SCC::init(n);
        for(int i = 1; i <= m; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            SCC::add_edge(u, v);
        }

        SCC::Kosaraju();

        if(SCC::n2 == 1) {
            printf("No
");
            continue;
        }

        printf("Yes
");
        SCC::solve();
    }
    return 0;
}

上述算法要跑1000ms,仔细想想是对DAG的多重边排序去重导致的,分析一下发现其实假如是DAG上面dp的话多重边其实无所谓的,正向图甚至也不用存。而这道题更绝,只需要找一个出度为0的缩后新点就可以了,连图都不用存。

注:貌似强连通缩点后只有一个顶点的次次都是特殊情况。Codeforces貌似可以有行末空格。

标签:强连通分量

参考资料:

Codeforces 1239B. The World Is Just a Programming Task (Hard Version) - LLTYYC - 博客园
Codeforces 1239D. Catowice City - LLTYYC - 博客园

原文地址:https://www.cnblogs.com/KisekiPurin2019/p/11817004.html