天天看点

曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay

版权属于yhzq,想要引用此题(包括题面)的朋友请联系博主

T2.Van

曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay

分析:

这样把博弈黑出翔真的好吗。。。

首先我们要明确怎么计算一个序列的最长Van序列

这就和花匠那道题有异曲同工之妙了

我们只需要找到序列的所有折点即可

k=1

这种情况下我们只能把序列中的每一个数变成一样的

显然都变成中位数最优

“中位数定理”的玄学证明

k=2

这种情况下我们需要使整个序列单调不升或者不降

于是我就想到了这道题

mmp,完全是原题好伐。。。不讲

k=3

枚举中间折点,转化成k=2的情况

k<=10

注意到这个数据的n很小,那么我们可以预处理使每个区间上升或下降的最小代价

然后就可以dp了

f[i][j][k] f [ i ] [ j ] [ k ] 表示第 i i 位,已经有了jj段,最后一段是上升还是下降

#include <cstdio>
#include <algorithm>
#include <cstring>
#define N 20010
#define M 1010
using namespace std;
int tot, a[N], n, k;
struct heap {
    int ch[N][], dis[N], val[N];
    int merge(int x, int y) {
        if (x * y == ) return x + y;
        if (val[x] < val[y]) swap(x, y);
        ch[x][] = merge(ch[x][], y);
        if (dis[ch[x][]] < dis[ch[x][]])
            swap(ch[x][], ch[x][]);
        dis[x] = dis[ch[x][]] + ;
        return x;
    }
    int pop(int x) {
        return merge(ch[x][], ch[x][]);
    }
    void init(int x, int v) {
        ch[x][] = ch[x][] = dis[x] = ;
        val[x] = v;
    }
} h;
struct seqs {
    int l, r, root, insum, innum, sum;
    int cost() {
        int v = h.val[root];
        return (v * innum - insum) + (sum - insum - v * (r - l +  - innum));
    }
    void merge(seqs now) { //now must behind this
        r = now.r, root = h.merge(root, now.root);
        innum += now.innum, insum += now.insum, sum += now.sum;
        while (innum *  > r - l + )
            innum--, insum -= h.val[root], 
            root = h.pop(root);
    }
} seq[N];
void add(int pos, int v, int &cost) {
    seq[++tot].root = seq[tot].l = seq[tot].r = pos;
    seq[tot].innum = , seq[tot].insum = seq[tot].sum = v;
    h.init(pos, v), cost += seq[tot].cost();
    while(tot >  && h.val[seq[tot].root] < h.val[seq[tot - 1].root]) {
        cost -= seq[tot].cost() + seq[tot - 1].cost();
        seq[tot - 1].merge(seq[tot]), tot--;
        cost += seq[tot].cost();
    }
}
void solve_1() {
    sort(a + , a +  + n);
    int ans = , mid = a[n / ];
    for (int i = ; i <= n; i++) ans += abs(a[i] - mid);
    printf("%d\n", ans);
}
void solve_2() {
    int ans = , cost;
    cost = , tot = ;
    for (int i = ; i <= n; i++) add(i, a[i], cost);
    ans = min(ans, cost);
    cost = , tot = ;
    for (int i = ; i <= n; i++) add(i, -a[i], cost);
    ans = min(ans, cost);
    printf("%d\n", ans);
}
void solve_3() {
    static int fwd1[N], fwd2[N], bac1[N], bac2[N];
    int ans = , cost;
    cost = tot = ;
    for (int i = ; i <= n; i++) add(i, a[i], cost), fwd1[i] = cost;
    cost = tot = ;
    for (int i = ; i <= n; i++) add(i, -a[i], cost), fwd2[i] = cost;
    cost = tot = ;
    for (int i = n; i >= ; i--) add(n - i + , a[i], cost), bac1[i] = cost;
    cost = tot = ;
    for (int i = n; i >= ; i--) add(n - i + , -a[i], cost), bac2[i] = cost;
    for (int i = ; i <= n + ; i++)
        ans = min(ans, fwd1[i - ] + bac1[i]), 
        ans = min(ans, fwd2[i - ] + bac2[i]);
    printf("%d\n", ans);
}
void solve_4() {
    k--;
    static int fwd[M][M], bac[M][M], f[M][], g[M][];
    for (int i = ; i <= n; i++) {
        int cost = ; tot = ;
        for (int j = i; j <= n; j++) add(j, a[j], cost), fwd[i][j] = cost;
        cost = , tot = ;
        for (int j = i; j <= n; j++) add(j, -a[j], cost), bac[i][j] = cost;
    }
    memset(f, , sizeof f), memset(g, , sizeof g);
    f[][] = g[][] = ;
    for (int i = ; i <= n; i++)
        for (int j = ; j <= k; j++)
            for (int l = ; l < i; l++)
                f[i][j] = min(f[i][j], g[l][j - ] + fwd[l + ][i]), 
                g[i][j] = min(g[i][j], f[l][j - ] + bac[l + ][i]);
    int ans = ;
    for (int i = ; i <= k; i++) ans = min(ans, f[n][i]);
    for (int i = ; i <= k; i++) ans = min(ans, g[n][i]);
    printf("%d\n", ans);
}
main() {
    freopen("van.in", "r", stdin);
    freopen("van.out", "w", stdout);
    scanf("%d%d", &n, &k);
    for (int i = ; i <= n; i++)
        scanf("%d", &a[i]);
    if (k == ) solve_1();
    else if (k == ) solve_2();
    else if (k == ) solve_3();
    else solve_4();
}
           

T3.Gay

曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay
曲神的hu测 T2.Van(左偏树+dp)T3.GayT2.VanT3.Gay

分析:

题目来源:33IQ

曲神表示:啊~原题是狗,我改成了博弈,没什么太大区别嘛

。。。博弈被黑的最惨的一次。。。

算法一

最简单的还是完全图的20分

如果第一天,有人走出去看到其他所有人的博弈都正常,那么就可以确定ta的博弈病变了,于是ta就会怀着沉重的心情用炒栗子超离子垃圾处理器把ta的博弈融掉

如果第一天,有人走出去看见一家的博弈病变了,但是到了这一天没有人去融掉博弈,说明这个住宅区中一定还存在一个病变的博弈,就是自己的那只?那个?那位?那坨?

反正第二天两个人会同时怀着沉重的心情用炒栗子超离子垃圾处理器把博弈融掉(嘶~~)

以此类推

因此有 x x 个病变博弈的社区需要xx天才能确定下来,进行融化

因此两问答案都是 ∑ni=1C(n,i)∗i ∑ i = 1 n C ( n , i ) ∗ i

算法二

上一个算法也还是挺有启发性的,我们发现我们在分析自己的博弈有没有病变的时候的方法是:假设没有病变,然后看前一天是否应该有熔化。而这个方法对任意图都适用的。

考虑状压dp,令 dpU d p U 表示 U U 集合中的博弈病变时的熔化时间

然后我们接着分析:如果我的博弈没病,那么什么时候应该熔化呢?

于是要枚举了所有可能的病变情况VV,那么如果他的博弈没有病变,最迟应该在

max dpV m a x   d p V 的时间有人熔化自己的博弈

所以他会在第 max dpV+1 m a x   d p V + 1 天熔化

所以我们就知道怎么DP了!

对于一个状态,我们枚举这个状态中每个病变的博弈的主人,那么这个状态的熔化时间应该是这些病变的博弈主人中熔化时间的最小值,从中我们也可以知道有多少人同时熔化

那么一个人会枚举哪些可能的病变情况呢:首先对于他看到的博弈,他枚举的病变情况一定是和当前情况相同的,其次他自己一定是没有病变的,而至于那些他看不到的博弈,他就只好枚举这些博弈的所有病变状况了

最后,如果有限时间内不会熔化怎么办?事实上是存在这种情况的,但是没关系,如果我们发现转移出现了环,所以这个环中的所有状态都是在有限时间内不会熔化的

时间复杂度似乎是 O(4nn) O ( 4 n n ) ?

可以通过前两个测试点

算法三

有了算法二,我们可以把表打出来看看,然后就可以发现一个小小的规律:

对于病变状态U和V,如果U∈V,那么必然有 dpU≤dpV d p U ≤ d p V

至于这一个规律的证明放在后面

所以就可以只枚举一种病变情况:首先对于所有他看到的博弈,他枚举的病变情况一定是和当前情况相同的,其次他自己一定是没有病变的,而至于那些他看不到的博弈,由刚才的规律,可以全部当成病变的

所以时间复杂度就可以降到 O(2nn) O ( 2 n n ) ,可以通过前四个测试点

算法四

为了取得更多的分,我们需要一个多项式算法

我们重新来考虑有限时间内熔化的条件:转移无环

实际上这是一个很强的条件

根据转移建一个新图,如果 i i 不能看到jj,那么就在 i i 到jj连条有向边

这个新图中可能有若干个强连通分量,如果一个病变状态中有病变的博弈的主人是在一个多于一个点的强连通分量中,那么这一个状态一定无法在有限时间内熔化(转移有环),否则一定能在有限时间内熔化(转移无环)

那么我们先把所有可以到达大小大于1的强连通分量的点连同和他们相关的边一起删掉,于是我们得到了一个DAG!

接着我们继续站在这张图的立场上来想怎么求一个状态的熔化时间:

首先这张图上有一些点被染黑了,黑点表示这个博弈有病,白点表示这个博弈没病

接着每一时刻,我们可以把一个黑点染白,然后把这个点连向的点的集合的一个子集染黑

若干轮之后,所有点都变白了

假设 k k 个点一度被染黑,那么熔化时间就是所有操作方案中最大的kk

看起来这个转化比较意识流,但是如果我们考虑怎么DP所有操作方案中最大的 k k ,我们会发现这个DP方法和算法二是等价的

如果再要解释的话,首先枚举博弈主人相当于枚举反转的黑点,算博弈主人熔化时间中的max体现在求最大的一度被染黑的点数,算状态的熔化时间中对博弈主人熔化时间取min体现在多次被染黑的点只被计算一次

而在这个模型下,算法三的小规律就很显然了

增加黑点对答案的贡献一定非负,所以问题又变成了:

一个DAG上有一些点被染黑了,接着每一时刻,我们可以把一个黑点染白,然后把这个点连向的点全部染黑。若干轮之后,所有点都变白了

假设有k个点一度被染黑,求所有操作方案中最大的kk

这不就是求DAG上一个点集能直接或者间接到达的点数吗?

所以我们把判断一个病变状态在第几天熔化转化为了求DAG上一个集合能直接或间接到达的点数。这样第一问已经非常简单了。考虑算每一个点的贡献,如果一个点能被 i i ​个点到达(包括自己),而DAG上一共有NN​个点,那么显然这个点的贡献就是 (2i−1)×2N−i ( 2 i − 1 ) × 2 N − i

接下来考虑第二问,一个人要在DAG上满足什么样的条件才会在第一天熔化呢?

在原来的DP中,我们是在取min的过程中得到同时熔化的人数的,而我们知道,在现在的模型中,算状态的熔化时间中对博弈主人熔化时间取min体现在多次被染黑的点只被计算一次。

所以我们脑补一下可以得到:如果一个点第一个反转,且之后得到的状态无论怎么操作都无法再把这个点染黑,那么这个点对应着一个最早熔化的博弈主人。

所以第二问也可以很简单的解决了,还是考虑算每一个点的贡献,如果一个点能被 i i 个点到达(包括自己),而DAG上一共有NN个点,那么显然这个点的贡献就是 2N−i 2 N − i

因为计算DAG中每一个点可以被多少点到达是 O(nm) O ( n m ) 的,而在这个DAG中 m m 是O(n2)O(n2)的,所以时间复杂度是 O(n3) O ( n 3 ) ,可以通过前六个点。

算法五

最后的技术含量就很低了,这个问题显然是可以压位的嘛,所以可以把时间复杂度搞到 O(n332) O ( n 3 32 ) ,可以通过所有测试点

#include <cstdio>
#include <bitset>
using namespace std;
typedef long long ll;
const int N = ;
const int mod = ;
bitset <N> b[N];
int d[N], que[N], g[N][N], n, m, t;
int pw2[N], ans1, ans2;
char s[N];
main() {
    freopen("gay.in", "r", stdin);
    freopen("gay.out", "w", stdout);
    scanf("%d", &n);
    for (int i = ; i <= n; ++i) {
        scanf("%s", s + ), s[i] = '1';
        for (int j = ; j <= n; ++j)
            if (s[j] == '0')
                d[i]++, g[i][j] = ;
    }
    pw2[] = ;
    for (int i = ; i <= n; ++i) pw2[i] = (pw2[i - ] << ) % mod;
    for (int i = ; i <= n; ++i) if (!d[i]) que[++t] = i;
    for (int i = ; i <= t; ++i)
        for (int j = , now = que[i]; j <= n; ++j)
            if (g[j][now] && !--d[j]) que[++t] = j;
    for (int i = t; i; --i) {
        int now = que[i];
        b[now][now] = ;
        int cnt = b[now].count();
        ans1 = (ans1 + l * (pw2[cnt] - ) * pw2[t - cnt]) % mod;
        ans2 = (ans2 + pw2[t - cnt]) % mod;
        for (int j = ; j <= n; ++j)
            if (g[now][j]) b[j] |= b[now];
    }
    printf("%d %d\n", ans1, ans2);
}