zoukankan      html  css  js  c++  java
  • Educational Codeforces Round 3

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

    A - USB Flash Drives

    送分题。

    B - The Best Gift

    送分题。

    C - Load Balancing

    题意:给一个数组,每次可以选一对数字,分别进行+1-1。使用最少次数的操作,达到最小的极差。

    题解:最小的极差要么是0要么是1。可以算出sum,然后算出上平均值和下平均值的值(和个数?根本不关心个数),把<下平均值的都变成下平均值,大于上平均值的都变成上平均值。

    int a[100005];
    
    void test_case() {
        int n;
        scanf("%d", &n);
        ll sum = 0;
        for(int i = 1; i <= n; ++i) {
            scanf("%d", &a[i]);
            sum += a[i];
        }
        ll LM = sum / n;
        ll RM = LM + 1;
        int cntRM = sum % n;
        ll ans = 0;
        for(int i = 1; i <= n; ++i) {
            if(cntRM > 0 && a[i] >= RM) {
                ans += a[i] - RM;
                --cntRM;
            } else if(a[i] >= LM)
                ans += a[i] - LM;
        }
        printf("%lld
    ", ans);
    }
    

    启示:注意只需要统计被-1的那一半就可以了,贪心选择能变成上平均值的先变完上平均值,剩下的全部都要变成下平均值(含排在后面的多出来的上平均值)。假算法用assert就知道假在哪里,相当于可以看到一点点数据。

    *D - Gadgets for dollars and pounds

    题意:有n天,给出n天的卢布到美元或者英镑的汇率,共有m种商品,每种商品是固定的美元价格和固定的英镑价格。你有s卢布,要求买k种商品。问是否可行,不可行输出-1。否则输出最小的需要的天数d,然后输出所购买的k种商品的编号以及他们分别在哪天购买。多种方案输出任意一种。

    题解:假如不要求最小的需要的天数d,莫非随便dp一下:设dp[i][j]为前i种商品中购买j种商品所需的最小价格,则转移为dp[i][j]=min(dp[i-1][j],dp[i][j-1]+mincost[i])?复杂度直接爆炸,但是很明显就不需要dp,因为明显可以贪心取出来。所以观察到这一点之后就懂了,首先最小的天数d满足单调性,因为天数更长只会让商品的最低价更有机会下降。所以可以二分枚举d,那么只需要验证d天内是否可以买k种商品。从前缀最小值中取出前d天中,单位美元所需的最少的卢布数,以及单位英镑所需的最少的英镑数。然后用这个数取遍历所有商品求出对应的最低的卢布价格,按卢布价格排序然后贪心取前面的前k个出来计算总价是否超过s。难度估计1800?

    仔细看输入发现看错题了,每种商品要么只能用美元买,要么只能用英镑买,问题不大。然后在输出的时候复杂度爆炸了,发现之后改过来,问题不大。发现忘记nth_element怎么用了,用个[1,100]数组c进行random_shuffle之后用nth_element进行测试,要使得c[k]==k成立的写法就是nth_element(c+1,c+k,c+1+n)。换句话说,从1开始计数的数组,取出第k小(k也是从1开始计数)需要用nth_element(c+1,c+k,c+1+n)。调用结束之后,恰好会使得c[k]就是数组的第k小(k也是从1开始计数)。

    仔细想想有很多地方可以省掉内存。

    假如用排序的话会多个log,但是这个log没有取满,大概只增加了40%的性能消耗。远远不及2000%。我猜测应该有一种类似“根号平衡”的策略,每次枚举的并不是中点,而是中点偏右的某个点,这样会不会使得期望复杂度总和下降呢?需要验证这个思路,要知道枚举一个答案,这个答案的正确概率有多大。也就是说要知道答案取值的概率分布。在这道题里面,枚举的值越大,答案是YES的概率也会上升。感觉这个问题应该蛮复杂的,有没有神仙可以解决这个问题。所以说这种带log的复杂度都是很玄学的,只能说使用kth_element的最坏情况下真的会取到mlogn,但是实际上跑起来却快得飞起。

    int n, m, k, s;
    int a[200005];
    int b[200005];
    
    struct Node {
        int id;
        int t;
        int c;
        ll s;
        Node() {}
        Node(int id, int t, int c, ll s): id(id), t(t), c(c), s(s) {}
        bool operator<(const Node& nd)const {
            return s < nd.s;
        }
    } nd[200005];
    
    bool check(int day) {
        int mina = a[day];
        int minb = b[day];
        for(int i = 1; i <= m; ++i) {
            if(nd[i].t == 1)
                nd[i].s = 1ll * nd[i].c * mina;
            else
                nd[i].s = 1ll * nd[i].c * minb;
        }
        nth_element(nd + 1, nd + k, nd + 1 + m);
        ll sum = 0;
        for(int i = 1; i <= k; ++i) {
            sum += nd[i].s;
            if(sum > s)
                return false;
        }
        return true;
    }
    
    int bs() {
        int L = 1, R = n;
        while(1) {
            int M = (L + R) >> 1;
            if(L == M) {
                if(check(L))
                    return L;
                if(R != L && check(R))
                    return R;
                return -1;
            }
            if(check(M))
                R = M;
            else
                L = M + 1;
        }
    }
    
    void test_case() {
        scanf("%d%d%d%d", &n, &m, &k, &s);
        for(int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        for(int i = 2; i <= n; ++i)
            a[i] = min(a[i], a[i - 1]);
        for(int i = 1; i <= n; ++i)
            scanf("%d", &b[i]);
        for(int i = 2; i <= n; ++i)
            b[i] = min(b[i], b[i - 1]);
        for(int i = 1; i <= m; ++i) {
            int t, c;
            scanf("%d%d", &t, &c);
            nd[i] = Node(i, t, c, 0);
        }
        int res = bs();
        if(res == -1) {
            printf("-1
    ");
            return;
        }
        printf("%d
    ", res);
        int daya = 0, dayb = 0;
        for(int i = 1; i <= k; ++i) {
            int day = 0;
            if(nd[i].t == 1) {
                if(daya == 0) {
                    int cost = nd[i].s / nd[i].c;
                    for(int i = 1; i <= n; ++i) {
                        if(a[i] == cost) {
                            daya = i;
                            break;
                        }
                    }
                }
                day = daya;
            } else {
                if(dayb == 0) {
                    int cost = nd[i].s / nd[i].c;
                    for(int i = 1; i <= n; ++i) {
                        if(b[i] == cost) {
                            dayb = i;
                            break;
                        }
                    }
                }
                day = dayb;
            }
            printf("%d %d
    ", nd[i].id, day);
        }
    }
    

    *E - Minimum spanning tree for each edge

    标签:最小生成树,树剖,ST表。

    题意:给一个n个点m条边的边带权无向图,对于每条边,求包含这条边在内的最小生成树的权值和。

    题解:这道题好像做过吧?求出整个图的最小生成树,对于某条边,假如他就在树上,则返回当前的权值和。否则要包含这条边(记为e(u,v)),则要断开u<->v路径上的一个权值最大的边。上面的两种情况可以合并,因为假如,某条边就在树上,那么权值最大的边就是他本身。所以变成一个在树上查询路径的最大值的问题,可以用树剖线段树或者树剖ST表去做。复制以前写过的那道题过来。

    https://www.cnblogs.com/KisekiPurin2019/p/12274986.html#_label4

    不过这个200行还是有点夸张。

    #define lc (o<<1)
    #define rc (o<<1|1)
    
    const int MAXN = 200000 + 5;
    int dep[MAXN], siz[MAXN],  son[MAXN], fa[MAXN], top[MAXN], tid[MAXN], rnk[MAXN], cnt;
    
    int n, m, r, mod;
    int a[MAXN];
    
    int head[MAXN], etop;
    
    struct Edge {
        int v, next;
        int w;
    } e[MAXN * 2];
    
    inline void init(int n) {
        etop = 0;
        memset(head, -1, sizeof(head[0]) * (n + 1));
    }
    
    inline void addedge(int u, int v, int w) {
        e[++etop].v = v;
        e[etop].w = w;
        e[etop].next = head[u];
        head[u] = etop;
        e[++etop].v = u;
        e[etop].w = w;
        e[etop].next = head[v];
        head[v] = etop;
    }
    
    int faE[MAXN + 5];
    
    struct SparseTable {
        static const int MAXLOGN = 19;
        static const int MAXN = 200000;
        int n, logn[MAXN + 5];
        int f[MAXN + 5][MAXLOGN + 1];
    
        void Init1() {
            logn[1] = 0;
            for(int i = 2; i <= MAXN; i++)
                logn[i] = logn[i >> 1] + 1;
        }
    
        void Init2(int _n) {
            n = _n;
            for(int i = 1; i <= n; i++)
                //树剖时应替换本身在线段树中build位置的东西
                f[i][0] = faE[rnk[i]];
            for(int j = 1, maxlogn = logn[n]; j <= maxlogn; j++) {
                for(int i = 1; i + (1 << j) - 1 <= n; i++)
                    f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
            }
        }
    
        int Query(int l, int r) {
            int s = logn[r - l + 1];
            return max(f[l][s], f[r - (1 << s) + 1][s]);
        }
    } st;
    
    void init1() {
        dep[r] = 1;
    }
    
    void dfs1(int u, int t, int w) {
        siz[u] = 1, son[u] = -1, fa[u] = t;
        faE[u] = w;
        for (int i = head[u]; i != -1; i = e[i].next) {
            int v = e[i].v;
            if(v == t)
                continue;
            dep[v] = dep[u] + 1;
            dfs1(v, u, e[i].w);
            siz[u] += siz[v];
            if (son[u] == -1 || siz[v] > siz[son[u]])
                son[u] = v;
        }
    }
    
    void init2() {
        cnt = 0;
    }
    
    void dfs2(int u, int t) {
        top[u] = t;
        tid[u] = ++cnt;
        rnk[cnt] = u;
        if (son[u] == -1)
            return;
        dfs2(son[u], t);
        for (int i = head[u]; i != -1; i = e[i].next) {
            int v = e[i].v;
            if(v == fa[u] || v == son[u])
                continue;
            dfs2(v, v);
        }
    }
    
    int CPTQuery(int u, int v) {
        int ret = -INF;
        int tu = top[u], tv = top[v];
        while (tu != tv) {
            if(dep[tu] > dep[tv]) {
                swap(u, v);
                tu = top[u], tv = top[v];
            }
            //对边树剖,没到同一重链的时候,要算上轻重链交错位置
            ret = max(ret, st.Query(tid[tv], tid[v]));
            v = fa[tv];
            tv = top[v];
        }
        //对边树剖,已到达同一重链上,不再算上高处的节点对应的边,也就是左区间+1,越界需要返回
        if(tid[u] == tid[v])
            return ret;
        if(dep[u] > dep[v])
            swap(u, v);
        ret = max(ret, st.Query(tid[u] + 1, tid[v]));
        return ret;
    }
    
    struct DisjointSetUnion {
        static const int MAXN = 200000;
        int n, fa[MAXN + 5], rnk[MAXN + 5];
    
        void Init(int _n) {
            n = _n;
            for(int i = 1; i <= n; i++) {
                fa[i] = i;
                rnk[i] = 1;
            }
        }
    
        int Find(int u) {
            int r = fa[u];
            while(fa[r] != r)
                r = fa[r];
            int t;
            while(fa[u] != r) {
                t = fa[u];
                fa[u] = r;
                u = t;
            }
            return r;
        }
    
        bool Merge(int u, int v) {
            u = Find(u), v = Find(v);
            if(u == v)
                return false;
            else {
                if(rnk[u] < rnk[v])
                    swap(u, v);
                fa[v] = u;
                rnk[u] += rnk[v];
                return true;
            }
        }
    } dsu;
    
    struct E2 {
        int w, u, v, id;
        bool operator<(const E2 &e)const {
            return w < e.w;
        }
    } e2[200005];
    
    ll ans[200005];
    
    void test_case() {
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= m; ++i) {
            scanf("%d%d%d", &e2[i].u, &e2[i].v, &e2[i].w);
            e2[i].id = i;
        }
    
        //建立最小生成树
        sort(e2 + 1, e2 + 1 + m);
        dsu.Init(n);
        init(n);
        int cnt = 0;
        ll sum = 0;
        for(int i = 1; i <= m; ++i) {
            if(dsu.Merge(e2[i].u, e2[i].v)) {
                addedge(e2[i].u, e2[i].v, e2[i].w);
                sum += e2[i].w;
                ++cnt;
                if(cnt == n - 1)
                    break;
            }
        }
    
        //对最小生成树进行树剖
        init1();
        int r = 1;
        dfs1(r, -1, -INF);
        init2();
        dfs2(r, r);
        st.Init1();
        st.Init2(n);
    
        //统计答案
        for(int j = 1; j <= m; ++j) {
            int ret = CPTQuery(e2[j].u, e2[j].v);
            ll tmp = sum - ret + e2[j].w;
            ans[e2[j].id] = tmp;
        }
        for(int j = 1; j <= m; ++j)
            printf("%lld
    ", ans[j]);
        return;
    }
    

    好像有更简单的方法,我们引入树剖的想法是在于使用线段树来支持修改,既然是不需要修改的,除了使用ST表改进复杂度以外,还可以使用树上倍增来求。每次在倍增求LCA的时候顺便维护到LCA的边的路径上的最大值。

    int n, m;
    
    struct E {
        int w, u, v, id;
        bool operator<(const E &e)const {
            return w < e.w;
        }
    } e[200005];
    
    struct DisjointSetUnion {
        static const int MAXN = 200000;
        int n, fa[MAXN + 5], rnk[MAXN + 5];
    
        void Init(int _n) {
            n = _n;
            for(int i = 1; i <= n; i++) {
                fa[i] = i;
                rnk[i] = 1;
            }
        }
    
        int Find(int u) {
            int r = fa[u];
            while(fa[r] != r)
                r = fa[r];
            int t;
            while(fa[u] != r) {
                t = fa[u];
                fa[u] = r;
                u = t;
            }
            return r;
        }
    
        bool Merge(int u, int v) {
            u = Find(u), v = Find(v);
            if(u == v)
                return false;
            else {
                if(rnk[u] < rnk[v])
                    swap(u, v);
                fa[v] = u;
                rnk[u] += rnk[v];
                return true;
            }
        }
    } dsu;
    
    vector<pii> G[200005];
    
    ll MST() {
        sort(e + 1, e + 1 + m);
        dsu.Init(n);
        ll sum = 0;
        for(int i = 1, cnt = 0; i <= m; ++i) {
            int u = e[i].u, v = e[i].v, w = e[i].w;
            if(dsu.Merge(u, v)) {
                G[u].push_back({v, w});
                G[v].push_back({u, w});
                sum += w;
                ++cnt;
                if(cnt == n - 1)
                    break;
            }
        }
        return sum;
    }
    
    int dep[200005];
    int fa[200005][20 + 1];
    int maxw[200005][20 + 1];
    
    void dfs(int u, int p, int faw) {
        dep[u] = dep[p] + 1;
        fa[u][0] = p;
        maxw[u][0] = faw;
        for(int i = 1; i <= 20; ++i) {
            fa[u][i] = fa[fa[u][i - 1]][i - 1];
            maxw[u][i] = max(maxw[u][i - 1], maxw[fa[u][i - 1]][i - 1]);
        }
        for(auto &e : G[u]) {
            if(e.first == p)
                continue;
            dfs(e.first, u, e.second);
        }
    }
    
    int LCA(int x, int y) {
        if (dep[x] < dep[y])
            swap(x, y);
        int res = -INF;
        for (int i = 20; i >= 0; i--) {
            if (dep[x] - (1 << i) >= dep[y]) {
                res = max(res, maxw[x][i]);
                x = fa[x][i];
            }
        }
        for (int i = 20; i >= 0; i--) {
            if (fa[x][i] != fa[y][i]) {
                res = max(res, maxw[x][i]);
                res = max(res, maxw[y][i]);
                x = fa[x][i], y = fa[y][i];
            }
        }
        if(x == y)
            return res;
        //x和y不同,但拥有相同的0级父亲,补上两条边
        return max(res, max(maxw[x][0], maxw[y][0]));
    }
    
    ll ans[200005];
    
    void test_case() {
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= m; ++i) {
            scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
            e[i].id = i;
        }
    
        //建立最小生成树
        ll sum = MST();
    
        //对最小生成树进行树上倍增
        dfs(1, 0, 0);
    
        //统计答案
        for(int j = 1; j <= m; ++j) {
            int ret = LCA(e[j].u, e[j].v);
            ll tmp = sum - ret + e[j].w;
            ans[e[j].id] = tmp;
        }
        for(int j = 1; j <= m; ++j)
            printf("%lld
    ", ans[j]);
    }
    

    注意:在使用倍增求LCA时,执行完第一个循环必有x和y深度相等,此时若x和y相等(x和y的LCA是其中之一)则直接返回,否则执行最后一个循环,然后必定有x和y的父亲相等,但x与y不相等,则说明还剩下x和y连接他们的LCA的最后两条边没加进去。

    *F - Frogs and mosquitoes

    看起来很像一棵线段树。

    题意:有很多只青蛙,第i只青蛙位于点xi,拥有长度为ti的右侧管辖范围,也就是[xi,xi+ti]。有若干只蚊子依次出现,第j只蚊子出现在点pj中。管辖范围包含pj的最左侧的青蛙会把这个蚊子吃掉,并且管辖范围的长度会增加这个蚊子的大小sj。若一只蚊子没有在出现时被吃掉,那么他会停留在原地。两只蚊子出现之间的间距很长,可以假设青蛙每次都会把可以吃掉的蚊子吃掉之后,下一只蚊子才会到来。

    题解:虽然看起来很线段树,但是实现上有一些细节,懒得想了,按敦爷说的抄答案也是学习的一种。需要先观察到一点:某只蚊子假如没有及时被吃掉,那么之后只有管辖范围变长的青蛙才有机会继续吃掉新的蚊子。并且这个过程可以不断循环(也就是不断吃新的蚊子),也就是说,假如对蚊子排序,在某只青蛙的管辖范围变长之后,需要查询pj>=xi的第一只蚊子,比较一下这个青蛙能不能吃掉他,若不能就退出,否则就吃掉他并继续验证是否可以继续吃,所以这里要用一个支持lower_bound的结构,当然就是set。蚊子的大小可以另开数组存,或者干脆开个set存。没有被及时吃掉的蚊子弄好了,再思考怎么快速查询某只蚊子有没有被及时吃掉?注意到青蛙管辖的区间是一个左端点不变的区间,而把蚊子及时吃掉,等价于右端点>=pj的最小的xi。神奇的发现线段树可以支持这种查询!先把左端点xi离散化,然后对每个xi开一个节点,里面存在其右端点xi+ti。那么这个“右端点>=pj的最小的xi”就可以通过维护线段树中区间的右端点的最大值来做。具体来说就是,先询问左半区间的右端点最大值是否>=pj,若是,则最小的ai必定在左子树中,否则一定不在左子树中,去右子树看看有没有。

    启示:区间的左端点不变,修改右端点的问题,很像线段树维护的东西。尤其像是“右端点>=x的左端点最小的区间”这种表述,用在线段树上二分就可以解决。需要注意没有被及时吃掉的蚊子,一定是在左边的某只青蛙的管辖范围变长的瞬间才有可能被吃掉,而且需要注意到可以人为给蚊子排序,每次取出最有机会被当前更新的青蛙吃掉的蚊子来验证。也就是说发现蚊子之间的次序关系,不要每次都暴力找所有的没被吃掉的蚊子。

    const int MAXN = 200000;
    
    struct Node {
        int x;
        int id;
        ll t;
        int cnt;
    } nd[MAXN + 5];
    
    bool cmpX(const Node& nd1, const Node& nd2) {
        return nd1.x < nd2.x;
    }
    
    bool cmpID(const Node& nd1, const Node& nd2) {
        return nd1.id < nd2.id;
    }
    
    struct SegmentTree {
    #define ls (o<<1)
    #define rs (o<<1|1)
        ll ma[(MAXN << 2) + 5];
    
        void PushUp(int o) {
            ma[o] = max(ma[ls], ma[rs]);
        }
    
        void Build(int o, int l, int r) {
            if(l == r)
                ma[o] = nd[l].x + nd[l].t;
            else {
                int m = (l + r) >> 1;
                Build(ls, l, m);
                Build(rs, m + 1, r);
                PushUp(o);
            }
        }
    
        void Update(int o, int l, int r, int p, ll v) {
            if(l == r) {
                ma[o] = v;
                return;
            } else {
                int m = (l + r) >> 1;
                if(p <= m)
                    Update(ls, l, m, p, v);
                if(p >= m + 1)
                    Update(rs, m + 1, r, p, v);
                PushUp(o);
            }
        }
    
        int Query(int o, int l, int r, int v) {
            if(ma[o] < v)
                return -1;
            while(l != r) {
                int m = (l + r) >> 1;
                if(ma[ls] >= v) {
                    o = ls;
                    r = m;
                } else {
                    o = rs;
                    l = m + 1;
                }
            }
            return l;
        }
    #undef ls
    #undef rs
    } st;
    
    map<int, pair<int, ll> > Map;
    
    int n, m;
    
    void Solve() {
        int p, b;
        scanf("%d%d", &p, &b);
        int res = st.Query(1, 1, n, p);
        if(res == -1 || nd[res].x > p) {
            Map[p].first += 1;
            Map[p].second += b;
            return;
        }
        nd[res].cnt += 1;
        nd[res].t += b;
        while(1) {
            auto it = Map.lower_bound(nd[res].x);
            if(it == Map.end() || it->first > nd[res].x + nd[res].t)
                break;
            nd[res].cnt += it->second.first;
            nd[res].t += it->second.second;
            Map.erase(it);
        }
        st.Update(1, 1, n, res, nd[res].x + nd[res].t);
    }
    
    void test_case() {
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; ++i) {
            scanf("%d%d", &nd[i].x, &nd[i].t);
            nd[i].id = i, nd[i].cnt = 0;
        }
        sort(nd + 1, nd + 1 + n, cmpX);
        st.Build(1, 1, n);
        for(int i = 1; i <= m; ++i)
            Solve();
        sort(nd + 1, nd + 1 + n, cmpID);
        for(int i = 1; i <= n; ++i)
            printf("%d %lld
    ", nd[i].cnt, nd[i].t);
    }
    

    标签:线段树上二分

  • 相关阅读:
    [机器学习] k-近邻算法(knn)
    [博客] 博客园侧边栏公告设置访问人数及访客国家来源
    Ubuntu搭建hugo博客
    CodeForces
    Javaweb开发入门___1
    JDBC的学习
    Mysql的学习7___权限和数据库设计
    Mysql的学习6____事物,索引,备份,视图,触发器
    Mysql的学习5___Mysql常用函数,聚合函数,sql编程
    Mysql的学习3___数据的管理,主键 外键 以及增改删
  • 原文地址:https://www.cnblogs.com/KisekiPurin2019/p/12466332.html
Copyright © 2011-2022 走看看