九省联考2018 题解

由于 \(\rm wqs\) 二分不太会,林克卡特树的 \(\rm dp\) 也没太看懂,所以这题先鸽着。

P4363 [九省联考 2018] 一双木棋 chess

两个人在 \(n\)\(m\) 列的棋盘上下棋,初始时没有棋子。一个棋子可以落在某一空格子,当且仅当其左侧及上方都没有棋子。棋盘满时游戏结束,如果设先手放置棋子的坐标集合为 \(A\),后手的为 \(B\),则他们的得分分别为:

\[\sum\limits_{(i,j)\in A} a_{i,j}\qquad \sum\limits_{(i,j)\in B}b_{i,j} \]

求二人都采取最优策略时先手得分减后手得分的值。(\(1\le n,m\le 10,0\le a_{i,j},b_{i,j}\le 10^5\))

感性理解可以发现,合法状态很少,真的很少,据题解区说仅有 \(35\rm w\) 种左右。所以我们只要能高效表示,转移合法状态,就可以在合理的时间复杂度内通过本题。考虑状态压缩,将当前棋盘的状态压成一个 \(11\) 进制数,并用记忆化搜索在不同的状态之间转移。博弈的话可以考虑 \(\min\max\) 对抗搜索,即先手要最大化这个差,后手要最小化这个差,先后手时用不同的转移即可。实现时判断先后手可以通过当前棋盘上有多少棋子看出,转移时枚举每一行,看看能不能在这一行的末尾再加一个棋子即可。时间复杂度 \(\mathcal{O}(?)\)

#include <map>
#include <cstdio>
const int N = 15; typedef long long ll; int a[2][N][N], n, m; std::map<ll, int> f; ll pw[N];
inline ll Pow(int a, int b) { ll ret = 1; for (int i = 1; i <= b; ++i) ret *= a; return ret; }
inline int getrow(ll x, int p) { return x / Pow(m + 1, p) % (m + 1); }
inline ll modify(ll x, int p, int v) { return x + Pow(m + 1, p) * v; }
inline int getsum(ll x) { int ans = 0; for (int i = 0; i < n; ++i) ans += getrow(x, i); return ans; }
int dfs(ll S)
{
    if (f.count(S)) return f[S]; 
    int d = getsum(S) & 1, ret = d ? 2e9 : -2e9; 
    for (int i = 0; i < n; ++i)
        if ((!i && getrow(S, i) < m) || getrow(S, i - 1) > getrow(S, i))
        {
            ll nS = modify(S, i, 1);
            if (d) ret = std::min(ret, dfs(nS) - a[d][i][getrow(S, i)]);
            else ret = std::max(ret, dfs(nS) + a[d][i][getrow(S, i)]);
        }
    return f[S] = ret;
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j) scanf("%d", &a[0][i][j]);
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j) scanf("%d", &a[1][i][j]);
    for (int i = 0; i <= n; ++i) pw[i] = Pow(m + 1, i);
    ll all = pw[n] - 1; f[all] = 0;
    printf("%d\n", dfs(0)); return 0;  
}

P4364 [九省联考 2018] IIIDX

\(n\) 个关卡,给出一个实数 \(k\),第 \(i\) 个关卡将会在第 \(\lfloor\frac{i}{k}\rfloor\) 个关卡后解锁,若 \(\lfloor\frac{i}{k}\rfloor=0\) 则无需解锁。给出一些难度 \(d_i\),给关卡适当分配难度,使得一个关卡解锁出的关卡不低于它的难度,若有多解,找出字典序最大的。(\(1\le n\le 5\times10^5,1\le k,d_i\le 10^9\))

显然解锁的关系形成了一个森林,而如果我们把 \(\lfloor\frac{i}{k}\rfloor=0\) 的情况看成需要 \(0\) 号关卡解锁,则就是棵树了,而我们要做的事情就是使父亲结点的权值均比儿子结点小,且尽量让编号靠前的权值大。注意到这满足小根堆的性质,考虑给 \(d_i\) 从大到小排序。而由于题目的规则,一棵子树内根一定编号最小,且一棵子树对应的一定是一段区间,所以如果想要字典序最大,权值也得是一段区间,且根是最小的那个。所以在结点 \(u\) 决策时,我们考虑按照它子树根节点的编号,将 \(u\) 对应的区间最大的分给编号最小的,以此类推。

这个做法看起来很对,但当 \(d_i\) 中存在相同元素会怎样呢?假如存在一个关系,\(u\)\(v\) 的父结点,\(x\)\(u\) 的兄弟结点,考虑出现这种关系:

\[d_v>d_x=d_u \]

根据之前的贪心策略,这种情况显然可能出现。而这种情况下,我们完全可以将 \(d_v,d_x\) 互换,在不破坏小根堆特点的情况下增大字典序。归根结底,这种情况还是由于我们的贪心完全没有考虑到父子结点权值相同的情况,导致子节点本可能更大的值因为被强制要求小于而被忽略。

考虑在这个贪心的基础上删删改改,下文依然默认 \(d_i\) 单调不增。考虑我们在给一个点 \(u\) 选择某个数时,对这个数的要求是什么:

  • 至少存在 \(siz_u\) 个数小于等于 \(u\),其中 \(siz_u\)\(u\) 的子树大小。

因为我们得在这棵子树里放上这么多的值。这次因为子树之间的关系不存在了,我们考虑朴素从 \(1\)\(n\) 号结点贪心。对于当前处理到的结点 \(u\),我们要找到一个数满足:

  • 不小于至少 \(siz_u\) 个数,且最靠左。

而这显然可以用在线段树上二分轻松实现,维护有多少可用的数比当前数少,并维护这个值的区间最小值即可。选了之后呢?显然之后再贪心的时候不能选到个数 \(<siz_u\),所以我们不如直接将个数减去 \(siz_u\),表示这些数被占用了。然后进入子节点对应的区间时,我们该选这些点了,就得把占用状态解除,再加回来。所以本质上,我们维护的就是不断把一个父亲结点的限制打散放到子树上的过程。还有一点需要注意,那就是对相等段的处理。我们原来的贪心就这么挂的,注意到这段内所有数都是等价的,所以我们当然要选最靠右的那个,选完之后把它删掉就好。注意到我们只需要修改这一个数,因为这个连续段就连这个最靠右的数都选不上,其他数就更选不上了。最终时间复杂度 \(\mathcal{O}(n\log n)\)

#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 5e5 + 10; int d[N], cnt[N], fa[N], siz[N], vis[N]; 
std::vector<int> T[N]; int ans[N];
struct SegTree
{
    #define ls(k) (k << 1)
    #define rs(k) (k << 1 | 1)
    struct node{ int l, r, minx, tag; }h[N << 2];
    inline void pushup(int k) { h[k].minx = std::min(h[ls(k)].minx, h[rs(k)].minx); }
    inline void add(int k, int v) { h[k].minx += v; h[k].tag += v; }
    inline void pushdown(int k) { int v; if (v = h[k].tag) add(ls(k), v), add(rs(k), v), h[k].tag = 0; }
    void build(int k, int l, int r)
    {
        h[k].l = l; h[k].r = r;
        if (l == r) return h[k].minx = l, void();
        int mid = (l + r) >> 1; build(ls(k), l, mid); build(rs(k), mid + 1, r);
        pushup(k);
    }
    void change(int k, int x, int y, int v)
    {
        if (x <= h[k].l && h[k].r <= y) return add(k, v);
        pushdown(k); int mid = (h[k].l + h[k].r) >> 1;
        if (x <= mid) change(ls(k), x, y, v); if (mid < y) change(rs(k), x, y, v);
        pushup(k);
    }
    int query(int k, int x)
    {
        if (h[k].l == h[k].r) return h[k].minx >= x ? h[k].l : h[k].l + 1;
        pushdown(k);
        if (h[rs(k)].minx < x) return query(rs(k), x);
        else return query(ls(k), x);
    }
}sgt;
void dfs(int u)
{
    siz[u] = 1;
    for (auto v : T[u]) { if (v != fa[u]) dfs(v), siz[u] += siz[v]; }
}
int main()
{
    int n; double k; scanf("%d%lf", &n, &k);
    for (int i = 1; i <= n; ++i) scanf("%d", d + i);
    std::sort(d + 1, d + n + 1, [](const int& a, const int& b) { return a > b; });
    for (int i = n; i >= 1; --i)
        if (d[i] == d[i + 1]) cnt[i] = cnt[i + 1] + 1;
        else cnt[i] = 1;
    for (int i = 1; i <= n; ++i) fa[i] = (int)(i / k), T[fa[i]].push_back(i);
    dfs(0); sgt.build(1, 1, n);
    for (int i = 1; i <= n; ++i)
    {
        if (fa[i] && !vis[fa[i]]) // 父亲结点的限制没去掉
        {
            vis[fa[i]] = 1;
            sgt.change(1, ans[fa[i]], n, siz[fa[i]] - 1);
        }
        int pos = sgt.query(1, siz[i]);
        pos = pos + cnt[pos] - 1; ++cnt[pos];
        ans[i] = pos; sgt.change(1, ans[i], n, -siz[i]); // 加上这个结点的限制
    }
    for (int i = 1; i <= n; ++i) printf("%d ", d[ans[i]]);
    puts(""); return 0;
}

P4365 [九省联考 2018] 秘密袭击 coat

给出一棵 \(n\) 个点,点带权 \(d_i\) 的树,求出这个树上的所有连通块第 \(k\) 大权值之和,答案对 \(64123\) 取模(如果点不足 \(k\) 个则记为 \(0\))(\(1\le n,k,d_i\le 1666,d_i\le W\))

看起来挺没头绪的,因为第 \(k\) 大这种东西我们完全没法处理,所以考虑把这个东西推一推式子:

\[\begin{aligned}&\sum_{S}\operatorname{kth}(S)\\=&\sum_{d=1}^Wd\sum_{S}[\operatorname{kth}(S)=d]\\=&\sum_{d=1}^W\sum_{S}[\operatorname{kth}(S)\ge d]\\=&\sum_{d=1}^W\sum_{S}[\operatorname{cnt}(S,d)\ge k]\end{aligned} \]

其中 \(\operatorname{cnt}(S,d)=\sum\limits_{x\in S}[x\ge d]\)。这个式子第一步转化是枚举第 \(k\) 大,第二步中,我们注意到,算 \(d\) 时也会顺便把比 \(d\) 大的数也算了一次,这样到 \(d'\) 时,其实已经算了 \(d'-1\) 次了,算上自己的一次刚好 \(d'\) 次,恰等于上一步。而最后一步转化就是利用了第 \(k\) 大的性质。

所以现在我们的任务就变为了对于每种权值,算出每个连通块中大于等于这个权值的数有至少 \(k\) 个的连通块个数,而这看起来就可做多了。因为是树上的连通块统计,所以我们考虑树形 \(\rm dp\),设 \(f_{u,i,j}\) 表示以 \(u\) 为根的子树,\(u\) 的权值强制选,大于等于权值 \(i\) 的数有 \(j\) 个的方案数。转移可以考虑背包:

\[f_{u,i,j}=\begin{cases}\prod\limits_{v\in son_u,\sum l_v=j}f_{v,i,l_v}&d_u<i\\\prod\limits_{v\in son_u,\sum l_v=j-1}f_{v,i,l_v}&d_u\ge i\end{cases} \]

最终答案即为:

\[\sum\limits_{1\le u\le n}\sum\limits_{1\le i\le W}\sum\limits_{k\le j\le n}f_{u,i,j} \]

直接转移可以做到 \(\mathcal{O}(nkW)\),常数比较小的话据说可以水过去。

当然,依靠这种看人品的方法是不靠谱的,我们考虑优化这个 \(\rm dp\)。首先我们能注意到这个转移其实就是个背包,完全可以用多项式乘法表示。但非常遗憾,出题人还特意表明不对 \(998,244,353\) 取模,用 \(\rm NTT\) 那一套的方法就被堵死了。但能算多项式乘法的方法可不止这一种,但其实不管怎么做都是先整成点值,然后再插回来。

我们首先把 GF 设出来,考虑设 \(F_{u,i}\) 为:

\[F_{u,i}(z)=\sum_{j=0}^n f_{u,i,j}z^j \]

\(\rm dp\) 转移用 GF 那一套系统就可以这样表达:

\[F_{u,i}(z)=\begin{cases}\prod\limits_{v\in son_u}(F_{v,i}(z)+1)&d_u<i\\z\prod\limits_{v\in son_u}(F_{v,i}(z)+1)&d_u\ge i\end{cases} \]

发现统计答案还需要对这个玩意求和,所以再设个 GF 表示求和的东西:

\[G_{u,i}(z)=\sum_{v\in son_u}F_{v,i}(z) \]

最终答案即为:

\[\sum_{d=1}^W\sum_{j=k}^n[z^j]G_{1,d}(z) \]

现在我们来考虑怎么多项式乘法。既然不能用 \(\rm NTT\) 那一套来多点求值和插值,我们就换个方法。首先多点求值我们完全不需要任何算法,直接枚举 \(n+1\)\(z\) 带进去算算值就行了,复杂度受得住。而对于插值,只要我们选择的 \(z\)\(n+1\) 个连续的数,我们就能用拉格朗日插值在 \(\mathcal{O}(n^2)\) 的时间复杂度内还原出原多项式的系数。思路有了之后,我们来分别看看怎么求值和插值。

因为多点求值时我们已经把多项式看成点值了,所以转移时直接对应乘就好了。接下来就是本题个人认为最神仙的地方了,考虑定义一个作用于二元组 \((f,g)\) 上的变换 \((a,b,c,d)\)

\[(f,g)\xrightarrow{(a,b,c,d)}(af+b,cf+d+g) \]

来看看这样做有什么意义。发现 \(af\) 相当于背包的两个点值相乘,\(b\) 是对 \(F\) 的微调,\(cf+g\) 就是统计 \(g\)\(d\) 是为了变换合并的方便。既然说到了变换的合并,我们来看看两个变换先后作用于二元组会怎么样吧(注意变换一般不满足交换律):

\[(af+b,cf+d+g)\xrightarrow{(A,B,C,D)}(Aaf+Ab+B,(Ca+c)f+Cb+d+g+D) \]

即:

\[(a,b,c,d)\cdot(A,B,C,D)=(Aa,Ab+B,Ca+c,Cb+d+D) \]

除此之外,还注意到变换的单位元为 \((1,0,0,0)\)。这有什么用呢?我们可以把一次背包的合并,一次统计答案等等我们需要的操作看做一次变换,作用于最初始的 \((0,0)\) 二元组。并用一些手段维护这些变换的合并,最后我们会得到一个最终变换,它的第四个元素对应的就是我们需要的 \(G\) 的点值。

我们先来找到各种操作需要什么变换。首先初始化 \(u\),我们要对于所有 \(i\) 都将 \(F_{u,i}(z)\) 初始化为 \(1\),不然乘个啥啊,而这个变换显然是:

\[(0,1,0,0) \]

然后通过某种方法将子树的背包合并上来了。接下来我们需要对于 \(\le d_u\)\(i\) 乘上一个 \(z\),而这个变换是:

\[(z,0,0,0) \]

然后是统计答案,将 \(F\) 加到 \(G\) 上:

\[(1,0,1,0) \]

最后我们要为它作为子节点的转移做好准备,给 \(F\) 加上常数 \(1\)

\[(1,1,0,0) \]

在刚刚的讨论中,提到了将子树的背包“合并”上来,从某种意义上来说,因为我们只要最终的变换即可,所以其实称这个过程为将子树的变换“合并”上来也是没有问题的。再加上刚刚我们需要只对一部分 \(F_u\) 做变换的限制,考虑对每个结点维护一棵线段树,线段树上维护的就是变换。每次往上合并的过程就是线段树合并的过程!最后询问整棵线段树的变换第四个元素之和即可得到 \(\sum\limits_{1\le i\le W}G_{1,i}(z)\)。这样,我们就成功在 \(\mathcal{O}(n^2\log W)\) 的时间复杂度内找到了 \(n+1\) 个点值。

好,接下来有了点值就该插值了。考虑拉格朗日插值的式子(证明可以考虑 \(\rm CRT\)):

\[f(x)=\sum_{i=1}^ny_i\prod_{i\ne j}\dfrac{x-x_j}{x_i-x_j} \]

相信大家都会给出连续的 \(x_i\)\(\mathcal{O}(n)\) 插出 \(f(k)\)\(k\) 为某个常数的值。所以这里介绍一下怎么在 \(\mathcal{O}(n^2)\) 的时间插出原多项式的系数。因为我们多点求值选的是连续的数 \(1\sim n+1\),考虑把式子变一变:

\[f(x)=\sum_{i=1}^ny_i\left(\prod_{i\ne j}(x-j)\left(\prod_{i\ne j}(i-j)\right)^{-1}\right) \]

注意到 \(-1\) 里面那个数比较好算,与 \(x\) 无关,不用管,最后乘就完事了。现在我们的问题聚焦在了这个式子:

\[\prod_{i\ne j}(x-j) \]

每次暴力乘是 \(\mathcal{O}(n^2)\) 的,时间复杂度不能接受。考虑先不考虑 \(i\ne j\) 的条件乘出来个玩意,然后每次除个 \((x-x_i)\)。假设我们已经乘到 \(p-1\) 了,目前得到的多项式是 \(f_{p-1}(x)\),则考虑 \(f_{p}(x)\) 与它的关系:

\[\begin{aligned}f_{p}(x)&=(x-p)f_{p-1}(x)\\&=xf_{p-1}(x)-pf_{p-1}(x)\end{aligned} \]

即将 \(f_{p-1}\) 乘个 \(p\) 然后减到上一位,这个过程可以从高到低枚举次数递推。而除法就是这东西的逆运算,除个 \(p\) 加到下一位就行了。

最后,我们把 \(k\sim n\) 次系数的和插出来即为答案,总时间复杂度 \(\mathcal{O}(n^2\log W)\)

#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 2e3 + 10, mod = 64123; typedef long long ll;
struct trans
{
    int a, b, c, d;
    inline void clear() { a = 1; b = c = d = 0; }
    trans() { this->clear(); }
    trans(int a, int b, int c, int d) : a(a), b(b), c(c), d(d) { }
    trans operator*(const trans& t) 
    { 
        int A, B, C, D;
        A = (ll)t.a * a % mod; B = ((ll)t.a * b + t.b) % mod;
        C = ((ll)t.c * a + c) % mod; D = ((ll)b * t.c + d + t.d) % mod;
        return trans(A, B, C, D); 
    }
};
struct SegTree
{
    int st[N << 6], tp, tn;
    struct node
    { 
        trans v; int ls, rs;
        inline void clear() { v.clear(); ls = rs = 0; }
        node() { this->clear(); }
    }h[N << 6];
    inline int getnode() { return tp ? st[tp--] : ++tn; }
    void del(int& k) 
    {
        if (!k) return ;
        del(h[k].ls); del(h[k].rs);
        st[++tp] = k; h[k].clear(); k = 0;
    }
    inline void pushdown(int k)
    {
        if (!h[k].ls) h[k].ls = getnode();
        if (!h[k].rs) h[k].rs = getnode();
        h[h[k].ls].v = h[h[k].ls].v * h[k].v;
        h[h[k].rs].v = h[h[k].rs].v * h[k].v;
        h[k].v.clear();
    }
    void change(int& k, int l, int r, int x, int y, trans v)
    {
        if (!k) k = getnode();
        if (x <= l && r <= y) return h[k].v = h[k].v * v, void();
        int mid = (l + r) >> 1; pushdown(k);
        if (x <= mid) change(h[k].ls, l, mid, x, y, v);
        if (mid < y) change(h[k].rs, mid + 1, r, x, y, v);
    }
    int query(int k, int l, int r)
    {
        if (l == r) return h[k].v.d;
        int mid = (l + r) >> 1, ret = 0; pushdown(k);
        (ret += query(h[k].ls, l, mid)) %= mod;
        (ret += query(h[k].rs, mid + 1, r)) %= mod;
        return ret;
    }
    int merge(int& k1, int& k2)
    {
        if (!k1 || !k2) return k1 + k2;
        if (!h[k1].ls && !h[k1].rs) std::swap(k1, k2);
        if (!h[k2].ls && !h[k2].rs)
        {
            h[k1].v = h[k1].v * trans(h[k2].v.b, 0, 0, 0);
            h[k1].v = h[k1].v * trans(1, 0, 0, h[k2].v.d);
            return k1;
        }
        pushdown(k1); pushdown(k2);
        h[k1].ls = merge(h[k1].ls, h[k2].ls);
        h[k1].rs = merge(h[k1].rs, h[k2].rs);
        return k1;
    }
}sgt;
std::vector<int> T[N]; int d[N], rt[N], y[N], xi[N], t[N], inv[N], n, k, w;
void dfs(int u, int fa, int x)
{
    sgt.change(rt[u], 1, w, 1, w, trans(0, 1, 0, 0));
    for (auto v : T[u])
    {
        if (v == fa) continue;
        dfs(v, u, x); sgt.merge(rt[u], rt[v]);
        sgt.del(rt[v]);
    }
    sgt.change(rt[u], 1, w, 1, d[u], trans(x, 0, 0, 0));
    sgt.change(rt[u], 1, w, 1, w, trans(1, 0, 1, 0));
    sgt.change(rt[u], 1, w, 1, w, trans(1, 1, 0, 0));
}
inline int work()
{
    int tmp, ret = 0; xi[0] = inv[1] = 1;
    for (int i = 2; i <= n + 1; ++i)
        inv[i] = (mod - (ll)(mod / i) * inv[mod % i] % mod) % mod;
    for (int i = 1; i <= n + 1; ++i)
    {
        for (int j = n + 1; j >= 1; --j)
            xi[j] = (ll)xi[j] * (mod - i) % mod, (xi[j] += xi[j - 1]) %= mod;
        xi[0] = (ll)xi[0] * (mod - i) % mod;
    }
    for (int i = 1; i <= n + 1; ++i)
    {
        for (int j = 0; j <= n + 1; ++j) t[j] = xi[j];
        for (int j = 0; j <= n; ++j)
            t[j] = mod - (ll)t[j] * inv[i] % mod, (t[j + 1] += mod - t[j]) %= mod;
        tmp = 0;
        for (int j = k; j <= n; ++j) (tmp += t[j]) %= mod;
        for (int j = 1; j <= n + 1; ++j)
        {
            if (i == j) continue;
            if (i > j) tmp = (ll)tmp * inv[i - j] % mod;
            else tmp = (ll)tmp * (mod - inv[j - i]) % mod;
        }
        (ret += (ll)y[i] * tmp % mod) %= mod;
    }
    return ret;
}
int main()
{
    scanf("%d%d%d", &n, &k, &w);
    for (int i = 1; i <= n; ++i) scanf("%d", d + i);
    for (int i = 1, x, y; i < n; ++i)
        scanf("%d%d", &x, &y), T[x].push_back(y), T[y].push_back(x);
    for (int x = 1; x <= n + 1; ++x)
    {
        dfs(1, 0, x); 
        y[x] = sgt.query(rt[1], 1, w);
        sgt.del(rt[1]);
    }
    printf("%d\n", work()); return 0;
}

P4382 [八省联考 2018] 劈配

\(n\) 名学员和 \(m\) 位导师,第 \(i\) 位导师的学员名额上限是 \(b_i\)。学员现在要选择导师,每人共要填 \(m\) 档志愿,每档最多填 \(C\) 位导师,可以不填。现在按照排名从高到低给学员编号,并从 \(1\)\(n\) 分别考虑每位学员的志愿。考虑到第 \(i\) 名学员时,会尽量录取他所填志愿中导师名额未满的最高档的导师,如果找不到这样的导师则他淘汰。求每名学员的第几档志愿会被满足,或报告他被淘汰。除此之外,每个学员还有期望值 \(s_i\),求每个人相对排名至少上升多少才能使他被第 \(s_i\) 或更高档的志愿录取,或报告无解。(\(1\le n,m\le 200,1\le C\le 10,0\le b_i\le n\))

看数据范围,两两匹配和一堆奇怪限制就知道这题是网络流题。一个比较好想的建模是之间从学员向导师连边,然后用费用乱搞加上优先级的限制,跑网络流。但这样不仅复杂度受不了,处理第二问也是个问题,所以我们考虑动态加边。

我们从 \(1\)\(n\) 分别考虑每个学员,考虑到第 \(i\) 个时,我们再从高到低考虑它每一档志愿,建出这档志愿对应的边,跑网络流。如果有流,则说明可以选到,保留这些边,退出枚举,并记录当前流量。(之后就是在残量网络上跑了)如果没有流量,则说明选不到,删除这些边继续跑。发现这样建模,我们既优先考虑了高名次学员,又优先考虑了高档志愿,还把影响保留了下来,且因为把没有用的边即使删掉了,复杂度也不会太爆炸。这样,我们就在 \(\mathcal{O}(?)\) 的时间复杂度内完成了第一问。

对于第二问,上升相对排名就相当于在他之前考虑的人变少了,因为发现不好删边,所以我们考虑从少到多枚举在他之前考虑的人,如果这次不行了,那就上一个就是答案。首先清空图,然后把所有 \(\ge s_i\) 的志愿的边建出来跑一遍网络流,相当于他上升到第一名了,如果这一遍都不行,那就怎么着也不行了。否则就开始枚举,加入第一名选择的边(只加入第一问中求出来的档对应的边),跑一遍网络流。如果得到的流量不比之前在求第一问时我们记录的流量多 \(1\)(即这一遍不可行了),那就停止枚举,记录答案就好了,否则保留边,继续加入第二名的,以此类推。这样,我们又用动态加边的网络流在 \(\mathcal{O}(?)\) 的时间复杂度内完成了第二问。

#include <queue>
#include <cstdio>
#include <cstring>
const int N = 7e3 + 10, M = 210; int d[N], cur[N], s, t;
struct edge{ int v, next, c; }E[N]; int p[N], rp[N], cnt, rnt;
inline void init() { memset(p, -1, sizeof (p)); cnt = 0; }
inline void insert(int u, int v, int c) { E[cnt].v = v; E[cnt].c = c; E[cnt].next = p[u]; p[u] = cnt++; }
inline void addedge(int u, int v, int c) { insert(u, v, c); insert(v, u, 0); }
inline bool bfs()
{
    std::queue<int> q; q.push(s); cur[s] = p[s];
    memset(d, -1, sizeof (d)); d[s] = 0;
    while (!q.empty())
    {
        int u = q.front(); q.pop();
        for (int i = p[u], v; i + 1; i = E[i].next)
        {
            v = E[i].v; cur[v] = p[v];
            if (d[v] == -1 && E[i].c) d[v] = d[u] + 1, q.push(v);
        }
    }
    return (d[t] != -1);
}
int dfs(int u, int flow)
{
    int ans = 0, ret; if (u == t) return flow;
    for (int i = cur[u], v; i + 1; i = E[i].next)
    {
        v = E[i].v; cur[u] = i;
        if (E[i].c && d[v] == d[u] + 1)
        {
            ret = dfs(v, std::min(flow, E[i].c));
            E[i].c -= ret; E[i ^ 1].c += ret;
            flow -= ret; ans += ret; if (!flow) break;
        }
    }
    if (!ans) d[u] = -1;
    return ans;
}
inline int dinic() { int ans = 0; while (bfs()) ans += dfs(s, 2e9); return ans; }
int a[M][M], b[M], ss[M], ans1[M], ans2[M], f[M], n, m;
inline void clear()
{
    init();
    for (int i = 1; i <= n; ++i) addedge(s, i, 1);
    for (int i = 1; i <= m; ++i) addedge(i + n, t, b[i]);
    rnt = cnt; memcpy(rp, p, sizeof (p));
}
int main()
{
    int T, c; scanf("%d%d", &T, &c);
    while (T--)
    {
        scanf("%d%d", &n, &m); t = n + m + 1;
        for (int i = 1; i <= m; ++i) scanf("%d", b + i);
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) scanf("%d", &a[i][j]);
        for (int i = 1; i <= n; ++i) scanf("%d", ss + i);
        clear(); int flow = 0, las = 0;
        for (int i = 1; i <= n; ++i)
        {
            int id = m + 1;
            for (int j = 1; j <= m; ++j)
            {
                for (int k = 1; k <= m; ++k) if (a[i][k] == j) addedge(i, k + n, 1);
                flow += dinic();
                if (flow != las) { id = j; las = flow; break; }
                else cnt = rnt, memcpy(p, rp, sizeof (rp));
            }
            ans1[i] = id; f[i] = flow; rnt = cnt; memcpy(rp, p, sizeof (p));
        }
        for (int i = 1; i <= n; ++i) printf("%d ", ans1[i]); puts("");
        for (int i = 1; i <= n; ++i)
        {
            if (ans1[i] <= ss[i]) { ans2[i] = 0; continue; }
            clear();
            for (int j = 1; j <= m; ++j) if (a[i][j] && a[i][j] <= ss[i]) addedge(i, j + n, 1);
            flow = dinic(); if (!flow) { ans2[i] = i; continue; }
            for (int j = 1; j < i; ++j)
            {
                for (int k = 1; k <= m; ++k) if (a[j][k] == ans1[j]) addedge(j, k + n, 1);
                flow += dinic();
                if (flow != f[j] + 1) { ans2[i] = i - j; break; }
            }
        }
        for (int i = 1; i <= n; ++i) printf("%d ", ans2[i]); puts("");
    }
    return 0;
}

P4383 [八省联考 2018] 林克卡特树

咕!

P4384 [八省联考 2018] 制胡窜

给出一个仅有数字字符的字符串,和 \(q\) 次询问 \(l,r\),每次询问需要回答存在多少个二元组 \((i,j)\) 满足 \(1\le i<j\le n\),且 \(i+1<j\),且 \(s_{l,r}\) 出现在 \(s_{1,i}\)\(s_{i+1,j-1}\)\(s_{j,n}\) 中。(\(1\le n\le 10^5,1\le q\le 3\times10^5\))

首先出现比较恶心(没错我大力分讨了一个下午没结果),所以我们考虑转化成不出现。也就是说,我们现在需要将原字符串切成三部分,且每一部分都不包含 \(s_{l,r}\)。我们发现。我们现在需要能够查询任意子串的出现位置集合才能比较好的回答这个问题,由于这个问题不是难点,所以我们考虑把它放到后面说。

好,现在我们假设我们知道了出现集合,现在来看看怎么分讨吧。首先,如果出现了三个互不相交的子串,则答案显然为 \(0\),因为不可能切把三个都切断。所以我们现在只需要讨论两种情况

  • 最左端和最右端的子串相交。
  • 最左端和最右端的子串不相交。

中间的子串我们就用一个子串代替了,因为其他的子串和这个子串表现一定是类似的,毕竟都要和最左最右相交。

当最左端和最右端的子串相交时,字符串表现大概是这样(下文中 \(m\) 表示子串个数):

\[l_1\quad l_2\quad l_m\quad r_1\quad r_2\quad r_m \]

图比较简陋感性理解吧。(

那我们来分讨 \(i\) 在哪,去找 \(j\) 的范围。

  1. \(1\le i<l_1\) 时,显然 \(s_{1,i}\) 是没有子串的,那我们只需要保证 \(s_{j,n},s_{i+1,j-1}\) 没有就好了,显然 \(j\) 要满足 \(l_m<j\le r_1\),也就是说,这一部分的贡献为:

    \[(l_1-1)(r_1-l_m) \]

  2. \(l_k\le i<l_{k+1}\) 时,\(s_{1,i}\) 还是没有子串,所以我们要限制 \(j\)\(s_{j,n}\) 对应的限制为 \(j>l_m\)\(s_{i+1,j-1}\) 对应的限制为 \(j\le r_{k+1}\),所以这部分的贡献为:

    \[\sum_{k=1}^{m-1}(l_{k+1}-l_k)(r_{k+1}-l_m) \]

    这部分有 \(l\)\(r\),看着很恶心,考虑全变成 \(r\)

    \[\sum_{k=1}^{m-1}(r_{k+1}-r_k)(r_{k+1}-l_m) \]

  3. \(l_m\le i<r_1\) 时,\(s_{1,i}\) 无子串,考虑限制 \(j\)\(s_{j,n},s_{i+1,j-1}\) 对应的限制均为 \(j>i+1\)。考虑分成两部分考虑,一部分 \(j\)\(i\) 贴的比较近,要考虑 \(j>i+1\)(此时 \(j\in[l_m,r_1]\)),另一种情况 \(i,j\) 比较远,无所谓 \(j>i+1\)(此时 \(j\in(r_1,n]\))分别讨论一下可以知道:

    \[\dbinom{r_1-l_m}{2}+(n-r_1)(r_1-l_m) \]

  4. \(r_1\le i\le n\) 时,\(s_{1,i}\) 有子串了,情况不合法。

总的来说,这一部分的情况为:

\[(l_1-1)(r_1-l_m)+\sum_{k=1}^{m-1}(r_{k+1}-r_k)(r_{k+1}-l_m)+\dbinom{r_1-l_m}{2}+(n-r_1)(r_1-l_m) \]

如果我们知道所有的结束位置,发现可能需要因为额外维护而头疼的只有这俩玩意:

\[\sum_{k=1}^{m-1}(r_{k+1}-r_k)r_{k+1};\quad \sum_{k=1}^{m-1}(r_{k+1}-r_k) \]

接下来我们来考虑最左端子串和最右端不相交的情况,图大概长这样:

\[l_1\quad l_2\quad r_1\quad l_m\quad r_2\quad r_m \]

还是一样,分讨 \(i\) 的位置,找 \(j\)

  1. \(1\le i<l_1\),这时 \(s_{1,i}\) 无子串,但 \(s_{j,n}\)\(s_{i+1,j-1}\) 的要求是冲突的(分别是 \(j> r_1,j\le r_1\)),所以情况不合法。
  2. \(l_k\le i<l_{k+1}\),这时 \(s_{1,i}\) 无子串,我们去找 \(j\)。与相交的情况不一样的是,可能会存在 \(r_1< l_{k+1}\) 的情况(比如上面画的 \(r_1<l_m\)),此时对于 \(j\) 的限制就不一样了。不过,当 \(l_m<r_{k+1},l_{k+1}<r_1\) 时,就跟相交的情况的等价了,注意 \(l_{k+1}\)\(r_{k+1}\) 是能相互转化的,即原条件相当于 \(l_m<r_{k+1}<r_1+len-1\)

    \[\sum_{k=1}^{m-1}(r_{k+1}-r_k)(r_{k+1}-l_m),l_m<r_{k+1}<r_1+len-1 \]

    比如对应到这个图,我们这种思路完全可以解决 \(l_1,l_2\) 之间 \(i\) 的对应问题。但对于 \(l_2,r_1\) 之间的,我们就无能为力了。当然,从这幅图来看,答案是比较显然的,只需要要求 \(l_m<j\le r_m\),所以方案数为:

    \[(r_1-l_2)(r_m-l_m) \]

    考虑将它推广至一般情况,则相当于找到 \(r_1+len-1\)\(r\) 集合中的前驱后继 \(p_1,p_2\),则产生的贡献为:

    \[(r_1-l_{p_1})(r_{p_2}-l_m) \]

总的来说,这一部分的情况为:

\[\sum_{k=1}^{m-1}(r_{k+1}-r_k)(r_{k+1}-l_m)[l_m<r_{k+1}<r_1+len-1]+(r_1-l_{p_1})(r_{p_2}-l_m) \]

注意到第一个式子和第一种情况类似,维护的东西不变。而第二个式子要求我们在 \(r\) 集合上查询查前驱后继。

现在我们分讨完了,接下来该维护信息了。刚刚我们埋下了一个小小伏笔,就是我们尽量把所有的东西朝 \(r\) 集合靠,而这个 \(r\) 集合,表示的是一个子串所有结束位置组成的集合。这是什么集合?\(\rm SAM\)\(\rm endpos\) 集合!而维护 \(\rm SAM\) 结点上的 \(\rm endpos\) 集合,是可以通过在 \(\rm parent\) 树上线段树合并实现的,线段树这种强大的数据结构,又支持我们维护刚刚需要的所有信息,所以这个思路是完全可行的。接下来我们有两个任务,一个是找到一个子串对应的 \(\rm SAM\) 结点,一个是用线段树维护那些信息。

找到一个子串 \(s_{l,r}\) 对应的 \(\rm SAM\) 结点是有套路的。考虑它一定被前缀 \(s_{1,r}\) 对应的结点包含,所以我们记录所有前缀对应的结点。然后由于 \(\rm SAM\) 是越向上能表示的最长子串是越来越短的,这样的话我们考虑从这个前缀结点向上查找,直到一个位置恰好把 \(s_{l,r}\) 包含进来,即 \(len\) 恰好(指的是这个结点再向上就不满足了)满足 \(\ge r-l+1\),这个位置就是我们想要的结点。这个过程可以用树上倍增优化。

而接下来我们要解决线段树如何维护信息。考虑不同于一般的 \(\rm SAM\) 上线段树合并问题,我们维护的值域线段树上值不是 \(0/1\),而是在 \(l\) 的位置值就是 \(l\),这样找前驱后继会非常方便,线段树上二分即可。而对于这两个式子:

\[\sum_{k=1}^{m-1}(r_{k+1}-r_k)r_{k+1};\quad \sum_{k=1}^{m-1}(r_{k+1}-r_k) \]

我们可以在 pushup 的时候,维护当前结点的这两个式子的值。新产生的贡献是跨过中间部分的 \(r_{k+1},r_k\),即左边的最大值和右边的最小值,计算上即可。

总的来说,此题我们需要一个值域线段树在 \(\rm SAM\)\(\rm parent\) 树上线段树合并统计 \(\rm endpos\),顺便维护信息。然后大力分讨用这些信息统计答案,时间复杂度 \(\mathcal{O}((n+q)\log n)\)

#include <cstdio>
#include <vector>
#include <cstdlib>
const int N = 2e5 + 10; typedef long long ll; char s[N]; int n, q;
struct SAM
{
    struct node{ int t[10], f, len; } a[N]; int tn, las;
    void insert(int c)
    {
        int p = las, np = ++tn; las = np;
        a[np].len = a[p].len + 1;
        for (; p && !a[p].t[c]; p = a[p].f) a[p].t[c] = np;
        if (!p) a[np].f = 1;
        else
        {
            int v = a[p].t[c];
            if (a[v].len == a[p].len + 1) a[np].f = v;
            else
            {
                int nv = ++tn; a[nv] = a[v];
                a[nv].len = a[p].len + 1;
                for (; p && a[p].t[c] == v; p = a[p].f) a[p].t[c] = nv;
                a[np].f = a[v].f = nv;
            }
        }
    }
} sam;
struct SegTree
{
    #define Ls(k) (h[k].ls)
    #define Rs(k) (h[k].rs)
    struct node { int ls, rs, minx, maxn; ll A, B; } h[N << 5], ret, tmp; int tn;
    inline void merge(const node& n1, const node& n2, node& n)
    {
        n.minx = std::min(n1.minx, n2.minx);
        n.maxn = std::max(n1.maxn, n2.maxn);
        n.A = n1.A + n2.A + (ll)n2.minx * (n2.minx - n1.maxn);
        n.B = n1.B + n2.B + (n2.minx - n1.maxn);
    }
    inline void copy(const node& sc, node& de) { de.A = sc.A; de.B = sc.B; de.minx = sc.minx; de.maxn = sc.maxn; }
    inline void pushup(int k) 
    {  
        if (Ls(k) && Rs(k)) merge(h[Ls(k)], h[Rs(k)], h[k]);
        else if (Ls(k)) copy(h[Ls(k)], h[k]);
        else if (Rs(k)) copy(h[Rs(k)], h[k]);
    }
    void change(int &k, int l, int r, int p)
    {
        if (!k) k = ++tn;
        if (l == r) return h[k].minx = h[k].maxn = p, h[k].A = h[k].B = 0, void();
        int mid = (l + r) >> 1;
        if (p <= mid) change(Ls(k), l, mid, p);
        else change(Rs(k), mid + 1, r, p);
        pushup(k);
    }
    int Min(int k, int l, int r, int x, int y)
    {
        if (!k) return 1e9;
        if (x <= l && r <= y) return h[k].minx;
        int mid = (l + r) >> 1, ret = 1e9;
        if (x <= mid) ret = std::min(Min(Ls(k), l, mid, x, y), ret);
        if (mid < y) ret = std::min(Min(Rs(k), mid + 1, r, x, y), ret);
        return ret;
    }
    int Max(int k, int l, int r, int x, int y)
    {
        if (!k) return 0;
        if (x <= l && r <= y) return h[k].maxn;
        int mid = (l + r) >> 1, ret = 0;
        if (x <= mid) ret = std::max(Max(Ls(k), l, mid, x, y), ret);
        if (mid < y) ret = std::max(Max(Rs(k), mid + 1, r, x, y), ret);
        return ret;
    }
    int merge(int k1, int k2, int l, int r)
    {
        if (!k1 || !k2) return k1 | k2;
        int k = ++tn;
        if (l == r) return merge(h[k1], h[k2], h[k]), k;
        int mid = (l + r) >> 1;
        Ls(k) = merge(Ls(k1), Ls(k2), l, mid);
        Rs(k) = merge(Rs(k1), Rs(k2), mid + 1, r);
        return pushup(k), k;
    }
    void query(int k, int l, int r, int x, int y)
    {
        if (!k) return ;
        if (x <= l && r <= y)
        {
            if (!ret.minx) ret = h[k];
            else tmp = ret, merge(tmp, h[k], ret);
            return ;
        }
        int mid = (l + r) >> 1;
        if (x <= mid) query(Ls(k), l, mid, x, y);
        if (mid < y) query(Rs(k), mid + 1, r, x, y);
    }
}sgt;
std::vector<int> T[N]; int fa[21][N], rt[N], edp[N], P[N];
void dfs(int u)
{
    if (edp[u]) sgt.change(rt[u], 1, n, edp[u]);
    for (auto v : T[u])
        dfs(v), rt[u] = sgt.merge(rt[u], rt[v], 1, n), fa[0][v] = u;
}
inline int find(int l, int r)
{
    int p = P[r];
    for (int i = 20; ~i; --i)
        while (sam.a[fa[i][p]].len >= r - l + 1) p = fa[i][p];
    return p;
}
inline ll C(int n) {  return n < 2 ? 0ll : (ll)n * (n - 1) / 2; }
inline ll query(int l, int r)
{
    int len = r - l + 1, p = find(l, r);
    int L = sgt.h[rt[p]].minx, R = sgt.h[rt[p]].maxn;
    if (L < R - len * 2 + 1 && sgt.Max(rt[p], 1, n, L, R - len) - len + 1 > L) return C(n - 1);
    if (R - len + 1 <= L)
    {
        auto u = sgt.h[rt[p]]; int lm = R - len + 1;
        return C(n - 1) - u.A + u.B * lm - C(L - lm) - (ll)(L - lm) * (n - len);
    }
    else
    {
        sgt.ret.minx = sgt.ret.A = sgt.ret.maxn = sgt.ret.B = 0; 
        int lm = R - len + 1, pL = sgt.Max(rt[p], 1, n, 1, lm);
        sgt.query(rt[p], 1, n, pL, L + len - 1); auto u = sgt.ret;
        int p1 = sgt.Max(rt[p], 1, n, 1, L + len - 1), p2 = sgt.Min(rt[p], 1, n, L + len, n);
        return C(n - 1) - u.A + u.B * lm - (p2 > lm ? (ll)(L - p1 + len - 1) * (p2 - lm) : 0ll);
    }
}
int main()
{
    scanf("%d%d%s", &n, &q, s + 1); sam.las = sam.tn = 1;
    for (int i = 1; i <= n; ++i) sam.insert(s[i] - '0'), edp[sam.las] = i, P[i] = sam.las;
    for (int i = 2; i <= sam.tn; ++i) T[sam.a[i].f].push_back(i);
    dfs(1);
    for (int j = 1; j < 21; ++j)
        for (int i = 1; i <= sam.tn; ++i) fa[j][i] = fa[j - 1][fa[j - 1][i]];
    for (int i = 1, l, r; i <= q; ++i)
        scanf("%d%d", &l, &r), printf("%lld\n", query(l, r));
    return 0;
}

这篇文章写了 \(600\) 多行,预计算上林克卡特树还得再多一点。这就是 \(2018\) 吗。

posted @ 2022-03-16 21:40  zhiyangfan  阅读(73)  评论(0编辑  收藏  举报