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\n", 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\n");
        return;
    }
    printf("%d\n", 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\n", 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\n", 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\n", 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\n", nd[i].cnt, nd[i].t);
}

标签:线段树上二分

posted @ 2020-03-11 23:10  KisekiPurin2019  阅读(188)  评论(0编辑  收藏  举报