杭电多校第三场7月28日补题记录

B Destinations

题意:给定一棵有 n n n 个节点的树以及树上的 3 m 3m 3m 条链。这些链可以分成 m m m 组,每组 3 3 3 条,每组内链都有一个公共起点。每条链有权值(介于 [ 1 , 1 × 1 0 6 ] [1,1\times 10^6] [1,1×106]),求选择 m m m 条互不相交没有公共点的链的最小权值和,无解返回 -1

解法:考虑强制选满 m m m 条边如何限制。由于要满足链互不相交的条件,因而至多只能选择 m m m 条。那么为了让不选满的代价大过选择其他的链之和,我们让每条链的代价变成 ( m + 1 ) × 1 0 6 − v (m+1) \times 10^6-v (m+1)×106v,用 1 0 6 m 10^6 m 106m 进制下表示为 ( 1 , 1 0 6 − v ) (1,10^6-v) (1,106v)。那么显然怎么加,后面的值都无法进位到前面。在最后处理答案的时候,如果代价和在 1 0 6 m 10^6m 106m 进制下的第二位为 m m m 则证明合法(因为只有这样第二位才能累积到 m m m 1 1 1,而下面的位无法进位到上面),反之不合法。合法即取下面的位上的值,减去 1 0 6 m 10^6m 106m 即可。

解决了这个问题,回到原问题——选择若干条链,使得互相没有公共点,同时总权值最大。此题即为 HDU 5293。将这个问题转化为其镜像问题:在树上尽可能的少选点(允许重复选择一些点),使得这 3 m 3m 3m 链上每条链被选中的点的数目大于等于该链的权值。

这个使用一个贪心即可解决:从深到浅做 dfs,每次处理子树根节点的时候将子树全部处理完成再进行。考虑每一条以当前节点 u u u 为最浅节点(下记为链的 LCA)的链,若当前链上已经选择的点的数目不够,则全部在子树根处补齐,因而子树根上选取最大的、所需链的点的数目。

维护一个 f [ u ] f[u] f[u] 表示 u u u 到根上选取点的数目。那么判断链 ( u , v ) (u,v) (u,v) 上选取数目,若其 LCA 为 x x x,则已经选择点的数目为 f [ u ] + f [ v ] − f [ x ] f[u]+f[v]-f[x] f[u]+f[v]f[x]。考虑最后的子树根处新增点数目,只需要子树整体加即可。

考虑欧拉序列,可以将整棵树上一个子树映射到一段连续的区间上。那么问题进一步转化为——区间加、单点求值。这个又可以使用树状数组的差分解决。这种方法又叫树上差分。

考虑这样转化的合理性:转化后的问题是在 3 m 3m 3m 条链上选取尽可能少的点。由于这 3 m 3m 3m 条链共用了一些起点,那么一个可行的方案是:我只在这些起点上重复的选取起点,直到这个起点上三条链的权值全部都满足。记这个答案为 a n s ans ans。记第 i i i 组的三条链分别为 l i , 1 , l i , 2 , l i , 3 l_{i,1},l_{i,2},l_{i,3} li,1,li,2,li,3,其权值分别为 w i , 1 , w i , 2 , w i , 3 w_{i,1},w_{i,2},w_{i,3} wi,1,wi,2,wi,3,那么容易得到, a n s = ∑ i = 1 m max ⁡ { w i , 1 , w i , 2 , w i , 3 } \displaystyle ans=\sum_{i=1}^{m} \max{ \{w_{i,1},w_{i,2},w_{i,3} \} } ans=i=1mmax{wi,1,wi,2,wi,3}

显然,如果使用贪心,记答案为 a n s ′ ans' ans,答案不会比这个答案更大,即 a n s ′ ≤ a n s ans' \leq ans ansans。回到原问题,由于我们现在要选取的 m m m 条边互不相交,且总权值最大,也就是说只能在 m m m 组里每组各选一个,那么答案至少为 a n s = ∑ i = 1 m max ⁡ { w i , 1 , w i , 2 , w i , 3 } \displaystyle ans=\sum_{i=1}^{m} \max{ \{w_{i,1},w_{i,2},w_{i,3} \} } ans=i=1mmax{wi,1,wi,2,wi,3}。那么容易发现,贪心算法出来的答案,满足 a n s = ∑ i = 1 m max ⁡ { w i , 1 , w i , 2 , w i , 3 } ≤ a n s ′ ≤ a n s = ∑ i = 1 m max ⁡ { w i , 1 , w i , 2 , w i , 3 } \displaystyle ans=\sum_{i=1}^{m} \max{ \{w_{i,1},w_{i,2},w_{i,3} \} } \leq ans' \leq \displaystyle ans=\sum_{i=1}^{m} \max{ \{w_{i,1},w_{i,2},w_{i,3} \} } ans=i=1mmax{wi,1,wi,2,wi,3}ansans=i=1mmax{wi,1,wi,2,wi,3},即 a n s ′ = a n s = ∑ i = 1 m max ⁡ { w i , 1 , w i , 2 , w i , 3 } ans'=\displaystyle ans=\sum_{i=1}^{m} \max{ \{w_{i,1},w_{i,2},w_{i,3} \} } ans=ans=i=1mmax{wi,1,wi,2,wi,3}。因而原问题可以进行这样的转化。

整体复杂度 O ( ( n + m ) log ⁡ n ) O((n+m) \log n) O((n+m)logn)

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
long long base;
struct line
{
    int from;
    int to;
    int next;
};
struct line que[400005];
struct node
{
    int u;
    int v;
    long long cost;
};
struct node plan[600005];
int cnt, headers[200005];
vector<node> root[200005];
void add(int from,int to)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
int st[200005], ed[200005], tot;
int fa[35][200005], depth[200005];
int lg[200005];
void dfs(int place,int father)//欧拉序列+lca的预处理
{
    st[place] = ++tot;
    depth[place] = depth[father] + 1;
    fa[0][place] = father;
    for (int i = 1; (1 << i) <= depth[father]; i++)
        fa[i][place] = fa[i - 1][fa[i - 1][place]];
    for (int i = headers[place]; i; i = que[i].next)
        if (que[i].to != father)
            dfs(que[i].to, place);
    ed[place] = ++tot;
}
int lca(int a,int b)
{
    if (depth[a] < depth[b])
        swap(a, b);
    while(depth[a]>depth[b])
        a = fa[lg[depth[a] - depth[b]] - 1][a];
    if (a == b)
        return a;
    for (int k = lg[depth[a]]; k >= 0; k--)
        if (fa[k][a] != fa[k][b])
        {
            a = fa[k][a];
            b = fa[k][b];
        }
    return fa[0][a];
}
long long f[400005];
int lowbit(int x)
{
    return x & (-x);
}
void update(int x,long long k)
{
    while(x<=tot && x)
    {
        f[x] += k;
        x += lowbit(x);
    }
    return;
}
long long query(int x)
{
    long long ans = 0;
    while(x)
    {
        ans += f[x];
        x -= lowbit(x);
    }
    return ans;
}
long long solve(int place,int father)//主体 dfs 过程,返回以 place 为子树的最少选取点的数目
{
    long long ans = 0;
    for (int i = headers[place]; i; i = que[i].next)
        if(que[i].to!=father)
            ans += solve(que[i].to, place);
    long long maximum = 0;
    for (auto i : root[place])
    {
        long long temp = query(st[i.u]) + query(st[i.v]) - query(st[place]);
        maximum = max(maximum, base + 1000000 - i.cost - temp);//记得边权要按照第一条说的进行转化
    }
    ans += maximum;
    update(st[place], maximum);
    update(ed[place] + 1, -maximum);
    return ans;
}
int main()
{
    for (int i = 1; i <= 200000; i++)
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    int t, n, m, u, v;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        base = (long long)m * 1000000ll;
        for (int i = 1; i <= n;i++)
            st[i] = ed[i] = headers[i] = depth[i] = 0;
        for (int i = 1; i <= n;i++)
            for (int j = 0; j <= 30;j++)
                fa[j][i] = 0;
        for (int i = 1; i <= tot;i++)
            f[i] = 0;
        cnt = tot = 0;
        for (int i = 1; i <= n;i++)
            root[i].clear();
        for (int i = 1; i < n;i++)
        {
            scanf("%d%d", &u, &v);
            add(u, v);
            add(v, u);
        }
        dfs(1, 1);
        int num = 0;
        for (int i = 1; i <= m;i++)
        {
            int s;
            scanf("%d", &s);
            for (int j = 1; j <= 3;j++)
            {
                num++;
                plan[num].u = s;
                scanf("%d%lld", &plan[num].v, &plan[num].cost);
                root[lca(plan[num].u, plan[num].v)].push_back(plan[num]);
            }
        }
        long long ans = solve(1, 1);
        if(ans/base==m)//最后转化答案以及判断是否可行
        {
            ans %= base;
            ans = base - ans;
        }
        else
            ans = -1;
        printf("%lld\n", ans);
    }
    return 0;
}

C Forgiving Matching

题意:给定仅由 09和通配符 *构成的长度为 n n n S S S 串和长度为 m m m T T T 串,问 T T T 整串与 S S S 中连续子串在错配次数等于 i i i 次的子串个数,需要求出每一个 i ∈ [ 0 , m ] i \in [0,m] i[0,m]

解法:显然, S S S 串中能和 T T T 整串匹配的位点仅有 n − m n-m nm 个。因而,我们只用考虑每一个位点的错配情况即可。

但是显然直接匹配无法承受,我们考虑分字符处理,即每个位点的错配情况等于在该位点下普通字符和通配符的贡献和。问题转化为每一种字符的匹配情况,即每一个位点下该字符精确匹配个数。

对于这类多次匹配问题,通常采用多项式的方法。对于第 i i i 个位置,认为是 x i − 1 x^{i-1} xi1 项或者 x − i + 1 x^{-i+1} xi+1 项,一组全正数次方匹配的另一组全负数次方;前面的系数是 0 0 0 1 1 1 表示当前这一项是否具有或者是否合法。

回到此题:对于一个字符来说,如果 S S S 串或者 T T T 串当前位置等于这个字符,那么就赋值成 1 1 1;反之则 0 0 0。这样就可以保证,只有当二者全为 1 1 1 的时候才能对答案贡献有 1 1 1 S S S 串的次方系数为正, T T T 串的次方系数为负,多次匹配过程可以浓缩到一次多项式乘法中,这样就可以保证二者在匹配的时候,对应位点成功匹配数体现在对应的项数的系数上。在这个方法下,我们很容易发现: x i − 1 x^{i-1} xi1 表示了 S S S 串的第 i i i 位开始匹配的当前字符匹配成功数。在实际操作中通常是直接让 T T T 串倒置,规避负数次方的问题。

因而对每一个普通字符都进行同样的操作。考虑通配符的影响。根据容斥原理,对于第 i i i 个位点,其影响为 T T T 串的全部通配符数加上 S S S 在这一段上的通配符数减去二者匹配的通配符数。而最后减去的数值,又是一个匹配问题,又可以转化成为多项式问题。

整体复杂度 O ( 11 n log ⁡ n ) O(11n \log n) O(11nlogn)

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <memory.h>
using namespace std;
const double pi = acos(-1);
struct complex
{
    double x;
    double y;
};
struct complex f[4000005], g[4000005];
int r[4000005];
complex operator +(complex a,complex b)
{
    return (complex){a.x + b.x, a.y + b.y};
}
complex operator -(complex a,complex b)
{
    return (complex){a.x - b.x, a.y - b.y};
}
complex operator *(complex a,complex b)
{
    return (complex){a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x};
}
void FFT(complex *J,int times,int flag)
{
    for (int i = 1; i < times;i++)
        if(i<r[i])
            swap(J[i], J[r[i]]);
    
    for (int i = 1; i < times; i <<= 1)
    {
        complex unit = (complex){cos(flag * pi / i), sin(flag * pi / i)};
        for (int j = 0; j < times; j += i * 2)
        {
            complex w = (complex){1, 0};
            for (int k = 0; k < i; k++, w = w * unit)
            {
                complex x = J[j + k], y = w * J[j + i + k];
                J[j + k] = x + y;
                J[j + k + i] = x - y;
            }
        }
    }
}
char a[200005], b[200005];
int suc[200005], sum[200005], ans[200005];
//suc 数组表示了 S 串的每个位点的成功匹配情况,以最后匹配位点为记录标准。
int main()
{
    int n, m, t;
    scanf("%d", &t);
    while(t--)
    {
        int times = 1, left = 0;
        scanf("%d%d", &n, &m);
        scanf("%s%s", a + 1, b + 1);
        reverse(b + 1, b + m + 1);
        sum[0] = 0;
        for (int i = 1; i <= n;i++)
        {
            sum[i] = sum[i - 1];
            if(a[i]=='*')
                sum[i]++;
        }
        int tot = 0;
        for (int i = 1; i <= m;i++)
            if(b[i]=='*')
                tot++;
        for (int i = 0; i <= n;i++)
            suc[i] = 0;
        for (int i = m; i <= n; i++)
        {
            suc[i] = tot + sum[i];
            if(i>=m)
                suc[i] -= sum[i - m];
        }
        while (times <= n + m)
        {
            times <<= 1;
            left++;
        }
        for (int i = 0; i < times; i++)
            r[i] = (r[i >> 1] >> 1) | ((i & 1) << (left - 1));
        for (int now = 0; now <= 10;now++)
        {
            char c;
            if(now<10)
                c = now + 48;
            else
                c = '*';
            for (int i = 0; i <= times;i++)
                f[i].x = g[i].x = f[i].y = g[i].y = 0;
            for (int i = 1; i <= n; i++)
                if (a[i] == c)
                    f[i - 1].x = 1;
            for (int i = 1; i <= m; i++)
                if (b[i] == c)
                    g[i - 1].x = 1;
            FFT(f, times, 1);
            FFT(g, times, 1);
            for (int i = 0; i <= times; i++)
                f[i] = f[i] * g[i];
            FFT(f, times, -1);
            for (int i = m; i <= n; i++)
                if(c=='*')
                    suc[i] -= (int)(f[i - 1].x / times + 0.5);
                //注意此处的f错位关系。x (x^{-1})=x^0,第一位和第一位匹配成功的结果存放在第 0 项,后面项数以此类推,因而错了一位
                else
                    suc[i] += (int)(f[i - 1].x / times + 0.5);
        }
        for (int i = 0; i <= m; i++)
            ans[i] = 0;
        for (int i = m; i <= n; i++)
            ans[m - suc[i]]++;//用桶来统计答案。
        for (int i = 0; i <= m; i++)
        {
            if (i)
                ans[i] += ans[i - 1];
            printf("%d\n", ans[i]);
        }
    }
    return 0;
}

F New Equipments II

题意:有 n n n 个工人和 n n n 个机器,存在 m m m 种不可匹配关系,其余的均可匹配。若一个工人 x x x 和一个机器 y y y 匹配,则获得 a x + b y a_x+b_y ax+by 的收益。问匹配了恰好 k k k 个人和机器的最大收益是多少,求出全部的 k ∈ [ 1 , n ] k \in [1,n] k[1,n] n ≤ 4 × 1 0 3 n \leq 4\times 10^3 n4×103 m ≤ 1 0 4 m \leq 10^4 m104

解法:考虑最朴素的网络流建图,新建源点汇点 S , T S,T S,T S S S 向第 i i i 个工人连一条容量为 1 1 1,费用为 a i a_i ai 的边,每台机器向 T T T 连一条 容量为 1 1 1,费用为 b i b_i bi 的边。中间只要没有不可匹配关系,统一连接一条工人到机器的容量为 1 1 1,费用为 0 0 0 的边。对于满足 k k k 个匹配关系,我们可以在源上卡一个 k k k 容量的边来限制。

但是直接这样时间是受不了的。考虑递增的求解问题,即每次扩大 1 1 1 的总流量之后得到的最大收益。这是一个增广路问题,按照一般网络流的方法,我们首先找到一条有容量的路,然后建立反边。那么我们考虑当前增广出来的路是怎么求得的。

出于贪心,对于右侧的一个机器,一定是找到当前能匹配上的最大价值的工人(下称左点)进行匹配。反过来考虑,我们按照 a i a_i ai 的大小关系,依次去 bfs 遍历右边那些点(机器点,下称右点),右点第一次被遍历到的左点就是我们要的点。

注意:经过增广之后,由于有反边的存在,因而这个图的深度发生了变化——初始时右边的点不可以遍历到左边去,但是现在不一样了。我们可以顺着这些反边又回到左边去。考虑这些反边的意义,它代表了之前的一次匹配关系,因而回到右边去的方法一定是基于之前匹配的结果。显然在 bfs 过程中,这样的边也要进行遍历延申。

这个题的第二个障碍在于图并不能完全建立—— O ( n 2 ) O(n^2) O(n2) 级别的边太多,无法承受。因而建立反图,只存储当前还有哪些点没有遍历到,存入 pool 中。被禁掉的边对应的节点加入 pool,而其余的点加入队列进行下一步的 bfs 遍历。对于新建立的反边,因为对应了一个匹配关系,因而这些边的出发点只有一个出边,因而没有必要在右点这里进行一次 bfs 过程,直接快进到将反边的终点进行遍历。一次 bfs 的复杂度仅 O ( n + m ) O(n+m) O(n+m),因为新建边数不超过 O ( n ) O(n) O(n)

那么现在我们已知了能遍历到的右点对应的权值最大的左点,在本轮增广中只保留最大的一个即可,加入答案。

最后,就是增广路后的建反边过程。虽说我们这一轮匹配了这样的两个点,但它们可能没有直连边——在被 ban 掉的关系中。我们可能是通过一系列的反边,才让这一对撮合,即在 bfs 中,扩展到右点的左点并不是直接最开头的那个。出现这种情况意味着我们需要用迭代对整条路上的匹配关系全部进行更换,进行一个网络流中的反悔操作。

整体复杂度 O ( n 2 + n m ) O(n^2+nm) O(n2+nm)

#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
#include <set>
using namespace std;
int match_right[4005], match_left[4005];//match_left指左点和右侧的谁匹配,match_right指右点和左侧的谁匹配
set<int> unfamiliar[4005];
int n;
bool vis[4005];
int pre[4005], from[4005];
void bfs(int x)
{
    if(vis[x])
        return;
    vis[x] = 1;
    queue<int> q;
    set<int> pool;
    for (int i = 1; i <= n;i++)//右点尚未被遍历的集合
        pool.insert(i);
    for (int i = 1; i <= n; i++)
        if (unfamiliar[x].count(i) == 0)//原点能匹配上的,直接记录
        {
            from[i] = x;//决定在本轮匹配点
            pre[i] = x;//bfs中由其增广到它的点
            pool.erase(i);
            q.push(i);
        }
    while(!q.empty())
    {
        int tp = q.front();
        q.pop();
        int former = match_right[tp];
        if(!former || vis[former])
            continue;
        vis[former] = 1;
        for(auto i:pool)
            if(unfamiliar[former].count(i)==0)
            {
                from[i] = x;
                pre[i] = former;
                pool.erase(i);
                q.push(i);
            }
    }
}
struct node
{
    int id;
    long long a;
    bool operator <(const node &b)const
    {
        return a > b.a;//按a值排序,进行遍历操作
    }
};
struct node que[4005];
long long a[4005], b[4005];
long long Augmenting_Path()
{
    long long best = -1;
    for (int i = 1; i <= n;i++)
        vis[i] = pre[i] = from[i] = 0;
    for (int i = 1; i <= n;i++)
        if(!match_left[que[i].id])//未匹配才进行bfs
            bfs(que[i].id);
    int this_time = 0;
    for (int i = 1; i <= n;i++)
        if(from[i] && !match_right[i])
            if(a[from[i]]+b[i]>best)
            {
                best = a[from[i]] + b[i];
                this_time = i;
            }
    if (best < 0)
        return -1;
    int former = from[this_time];
    while (pre[this_time] != former && this_time)//迭代反悔操作。可以手动操作一下
    {
        int temp = pre[this_time];
        match_right[this_time] = temp;
        swap(this_time, match_left[temp]);
    }
    match_left[former] = this_time;
    match_right[this_time] = former;
    return best;
}
int main()
{
    int t, m, u, v;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n;i++)
            unfamiliar[i].clear();
        for (int i = 1; i <= n;i++)
            match_left[i] = match_right[i] = 0;
        for (int i = 1; i <= n; i++)
        {
            scanf("%lld", &a[i]);
            que[i].id = i;
            que[i].a = a[i];
        }
        sort(a + 1, a + n + 1);
        for (int i = 1; i <= n;i++)
            scanf("%lld", &b[i]);
        for (int i = 1; i <= m;i++)
        {
            scanf("%d%d", &u, &v);
            unfamiliar[u].insert(v);
        }
        long long ans=0;
        for (int i = 1; i <= n; i++)
        {
            long long now = Augmenting_Path();
            if (now < 0)
            {
                for (int j = i; j <= n; j++)
                    printf("-1\n");
                break;
            }
            ans += now;
            printf("%lld\n", ans);
        }
    }
    return 0;
}

I Rise in Price

题意:给定一个 n × n n\times n n×n n ≤ 100 n \leq 100 n100) 的矩阵,每一个点 ( i , j ) (i,j) (i,j) 可以获取 a i , j a_{i,j} ai,j 的钻石,并且手上全部钻石升值 b i , j b_{i,j} bi,j。只能向上或向右走,问到终点后最大可以获得多少钱,即手上钻石数乘以钻石价格。数据保证随机。

解法:由于数据随机,因而要充分用好这个性质。

首先考虑最常见的暴力做法——将使用 DP 考察全部的 ( 2 n n ) \dbinom{2n}{n} (n2n) 种方案,每次从 ( i − 1 , j ) (i-1,j) (i1,j) ( i , j − 1 ) (i,j-1) (i,j1) 转移它们对应的全部方案数。显然这个直接暴力肯定是不行的。

考虑到数据随机,我们使用一个非常浅显的剪枝——如果对于一个节点下有两个方案 ( s u m a 1 , s u m b 1 ) (suma_1,sumb_1) (suma1,sumb1) ( s u m a 2 , s u m b 2 ) (suma_2,sumb_2) (suma2,sumb2) s u m a 1 < s u m a 2 suma_1<suma_2 suma1<suma2 并且 s u m b 1 < s u m b 2 sumb_1<sumb_2 sumb1<sumb2,那么显然前面这个方案完全没有前途——无论后面怎么走,都不可能有第二种方案优了,因而剪掉。因而,做完这个剪枝后,一个点全部的可行方案仅为 s u m a suma suma 单增, s u m b sumb sumb 单减的情况。由于数据随机,因而满足这种方式排列的数对期望个数仅 O ( n 2 log ⁡ n ) \displaystyle O(\frac{n^2}{\log n}) O(lognn2) 个。在此题下,大约数千个,完全在承受范围之内。因而仅使用这个剪枝即可顺利通过。

总体复杂度 O ( k n 2 ) O(kn^2) O(kn2) k k k 为一常数。

#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
long long a[105][105], b[105][105];
vector<pair<long long, long long>> f[105][105];
pair<long long, long long> pool[100005];//临时存储方案的池子
int place;
void add(pair<long long,long long> &now)//向池子中插入元素。需要保证suma单增sumb单减
{
    while(place && now.second>=pool[place].second)
        place--;
    if(!place || now.first>pool[place].first)
        pool[++place] = now;
}
void merge(vector<pair<long long,long long>> &x,vector<pair<long long,long long>> &y,vector<pair<long long,long long>> &z)
{
    int a = 0, b = 0;
    place = 0;
    while(a<x.size() && b<y.size())//按照归并排序的方式进行
    {
        if(x[a].first<y[b].first)
            add(x[a++]);
        else
            add(y[b++]);
    }
    while(a<x.size())
        add(x[a++]);
    while(b<y.size())
        add(y[b++]);
    z.clear();
    for (int i = 1; i <= place;i++)
        z.push_back(pool[i]);
    return;
}
int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        for (int i = 0; i <= n;i++)
            for (int j = 0; j <= n;j++)
                f[i][j].clear();
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                scanf("%lld", &a[i][j]);
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                scanf("%lld", &b[i][j]);
        f[1][1].push_back(make_pair(a[1][1], b[1][1]));
        for (int i = 1; i <= n;i++)
            for (int j = 1; j <= n;j++)
            {
                if(i==1 && j==1)
                    continue;
                else if(i==1)
                    f[i][j] = f[i][j - 1];
                else if(j==1)
                    f[i][j] = f[i - 1][j];
                else 
                    merge(f[i - 1][j], f[i][j - 1], f[i][j]);
                for(auto &s:f[i][j])
                {
                    s.first += a[i][j];
                    s.second += b[i][j];
                }
            }
        long long ans = 0;
        for(auto i:f[n][n])
            ans = max(ans, i.first * i.second);
        printf("%lld\n", ans);
    }
    return 0;
}

J Road Discount

题意:给定 n n n n ≤ 1 × 1 0 3 n \leq 1\times 10^3 n1×103)与 m m m m ≤ 2 × 1 0 5 m \leq 2\times 10^5 m2×105) 条边,边有两种边权——原价 c i c_i ci 与打折边权 d i d_i di,保证 c i ≥ d i c_i \geq d_i cidi,边权范围均在 [ 1 , 1 × 1 0 3 ] [1,1\times 10^3] [1,1×103]。问恰好使用 k k k 次打折边的最小生成树权值,需要求出全部 k ∈ [ 0 , n − 1 ] k \in [0,n-1] k[0,n1]

解法:此题的思想为 wqs(王钦石)二分,也称带权二分。其解决的问题为,当某一个条件刚好为 k k k 次的最优解。通常,这类问题不限制次数就会非常好做。使用 wqs 二分的条件为,若最优解记为 f ( k ) f(k) f(k),则 ( k , f ( k ) ) (k,f(k)) (k,f(k)) 构成一个凸包。

解决的方法为:二分斜率 k k k,然后找到斜率为 k k k 的切这个凸包的直线切于哪一点。在实现上,将问题中的某一个条件进行 k k k 的修改,忽略一切限制数目的条件,求出来一个 g ( x ) g(x) g(x)。此时这个 g ( x ) g(x) g(x) 表示变化了 k k k 的条件下最优解,解位于 x x x。这时我们可以根据 x x x 与 待求点横坐标 m m m 的关系——若 x > m x>m x>m 则减小斜率,反之增大。具体可见wqs二分详解

回到此题,此题其实不需要二分,因为每个点对应的最优解都要求出。考虑到边权仅在 [ 1 , 1 × 1 0 3 ] [1,1\times 10^3] [1,1×103] 的范围内,因而可以考虑直接枚举这个斜率 k ∈ [ − 1000 , 1000 ] k \in [-1000,1000] k[1000,1000]。此题中对应的修改是对打折边的修改——以打折边个数 x x x 为自变量,将全部的 d i d_i di 加上 k k k,然后对全部的打折边与原价边混在一起做最小生成树,跑出一个最小权值 g ( x ) g(x) g(x),记录下来——打折边个数与权值都要记录。最后统计答案的时候,找到第一个打折边数 x x x 小于等于 k k k 的,输出其权值减去修正量,即 g ( x ) − k x g(x)-kx g(x)kx。这是因为,此题中打折边个数 x x x 对应的最小生成树权值函数 f ( x ) f(x) f(x) 满足上凸条件,因而第一个满足条件的,即是与直线 y = g ( x ) − k x y=g(x)-kx y=g(x)kx 相切的,对应权值最小。

在实现上,显然边的数目仅有 O ( n ) O(n) O(n)——首先对全部原价边与全部打折边单独跑最小生成树,那么后面即使对全部的打折边都进行 k k k 的修正,选取的边也一定在这 2 n − 2 2n-2 2n2 条边中。

第二,在后面的生成树生成过程中,可以采用归并排序的方法进行实现——因为对于打折树边和原价树边,其权值均有序,因而可以归并排序的方法 O ( n ) O(n) O(n) 时间内完成建树。

整体复杂度 O ( k n + k c ) O(kn+kc) O(kn+kc)

#include <cstdio>
#include <algorithm>
#include <vector>
#include <memory.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int base = 1000;
int father[1005];
int getfather(int x)
{
    return x == father[x] ? x : father[x] = getfather(father[x]);
}
struct line
{
    int from;
    int to;
    int v;
    bool operator <(const line &b)const
    {
        return v < b.v;
    }
};
vector<line> tot[2], tree[2];
void Kruskal(int n,bool flag)
{
    for (int i = 1; i <= n; i++)
        father[i] = i;
    sort(tot[flag].begin(), tot[flag].end());
    int times = 0;
    for(auto i:tot[flag])
        if(getfather(i.from)!=getfather(i.to))
        {
            times++;
            father[getfather(i.from)] = getfather(i.to);
            tree[flag].push_back(i);
            if(times==n-1)
                break;
        }
    return;
}
void add(line now,int &weight,int &num,bool flag,int c)
{
    if(getfather(now.from)==getfather(now.to))
        return;
    father[getfather(now.from)] = getfather(now.to);
    weight += now.v;
    if(flag)
    {
        weight += c;
        num++;
    }
}
struct node
{
    int num;
    int weight;
    bool operator <(const node &b)const
    {
        return num < b.num;
    }
};
struct node ans[2005];
int main()
{
    int t, n, m;
    scanf("%d", &t);
    while(t--)
    {
        tot[0].clear();
        tot[1].clear();
        tree[0].clear();
        tree[1].clear();
        memset(ans, 0, sizeof(ans));
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= m;i++)
        {
            int u, v, c, d;
            scanf("%d%d%d%d", &u, &v, &c, &d);
            tot[0].push_back((line){u, v, c});
            tot[1].push_back((line){u, v, d});
        }
        Kruskal(n, 0);
        Kruskal(n, 1);
        for (int c = -1000; c <= 1000;c++)
        {
            for (int i = 1; i <= n;i++)
                father[i] = i;
            int placeold = 0, placenew = 0;
            while(placeold<tree[0].size() && placenew<tree[1].size())//类似归并排序的方式从小到大的遍历这两种树边
            {
                if (tree[0][placeold].v <= tree[1][placenew].v + c)
                {
                    add(tree[0][placeold], ans[c + base].weight, ans[c + base].num, 0, c);
                    placeold++;
                }
                else
                {
                    add(tree[1][placenew], ans[c + base].weight, ans[c + base].num, 1, c);
                    placenew++;
                }
            }
            while(placeold<tree[0].size())
            {
                add(tree[0][placeold], ans[c + base].weight, ans[c + base].num, 0, c);
                placeold++;
            }
            while(placenew<tree[1].size())
            {
                add(tree[1][placenew], ans[c + base].weight, ans[c + base].num, 1, c);
                placenew++;
            }
        }
        for (int k = 0; k < n;k++)
            for (int c = -1000; c <= 1000; c++)
                if(ans[c+base].num<=k)//第一个满足条件的,即是答案。
                {
                    printf("%d\n", ans[c + base].weight - k * c);//记得减掉修改量
                    break;
                }
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值