码迷,mamicode.com
首页 > 其他好文 > 详细

JLOI2015 DAY2 简要题解

时间:2019-02-10 21:52:17      阅读:146      评论:0      收藏:0      [点我收藏+]

标签:转移   emc   决定   复杂   +=   滚动   二叉树   题意   ems   

「JLOI2015」骗我呢

题意

问有多少个 \(n \times m\) 的矩阵 \(\{x_{i, j}\}\) 满足

  • 对于 \(\forall i \in [1, n], j \in [1, m]\)\(x_{i, j} \in[0, m]\)
  • 对于 \(\forall i \in [1, n], j \in [1, m)\)\(x_{i, j} < x_{i, j + 1}\)
  • 对于 \(\forall i \in (1, n], j \in [1, m)\)\(x_{i, j} < x_{i - 1, j + 1}\)

答案对于 \(10^9+7\) 取模。 \(1 \le m, n \le 10^6\)

题解

可以先写个暴力,观察下最后矩阵的性质。

不难发现对于每一行,会从小到大出现 \(m\) 个不同的数,那么只会有 \([0, m]\) 中的一个数不会在当行出现。

那么我们可以设一个很显然的 \(dp\) 了,令 \(f_{i, j}\) 为到第 \(i\) 行缺的是 \(j\) 的方案数。画画图,观察观察。每次可以转移的上一行的范围就是 \([j + 1, m]\)

然后利用前缀和优化就可以做到 \(O(n^2)\) ,优化后的 \(dp\) 本质上其实就是

有一个 长为 \(n + m + 1\) 宽为 \(n\) 的网格,从左下走到右上,每次只能向右或者向上走,不能经过 \(y = x + 1\)\(y = x - (m + 2)\) 的方案数。

这个其实是个经典计数,就是卡特兰数多了上下界的限制。

我们记通过 \(y = x + 1\) 的事件为 \(A\) 通过 \(y = x - (m + 2)\) 的事件为 \(B\) ,我们其实就是求不出现 \(A\)\(B\)

这个怎么做呢,通常可以考虑容斥。如果没有限制,走到 \((x, y)\) 的方案其实就是 \(\displaystyle {x + y \choose y}\)

那么我们考虑枚举它的一个交错子序列 \(ABABAB\dots\)

答案其实就是 \(All - A - B + AB + BA - BAB - ABA + \dots\)

那么现在问题就在与如何计算 \(ABABAB\dots\) 出现的方案数,我们考虑最后的终点 \((n + m + 1, n)\) 沿着 \(A\) 翻折 再沿着 \(B\) 翻折,这样不断重复下去。然后计算一开始起点到终点的方案数。

这样就可以在 \(\mathcal O(n)\) 的时间内解决啦。

总结

多记经典计数 \(dp\) ,关键时刻可以救命。

代码

#include <bits/stdc++.h>

#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
    freopen ("2109.in", "r", stdin);
    freopen ("2109.out", "w", stdout);
#endif
}

const int N = 5e6 + 1e3, Mod = 1e9 + 7;

int fac[N], ifac[N];

inline int fpm(int x, int power) {
    int res = 1;
    for (; power; power >>= 1, x = 1ll * x * x % Mod)
        if (power & 1) res = 1ll * res * x % Mod;
    return res;
}

void Math_Init(int maxn) {
    fac[0] = ifac[0] = 1;
    For (i, 1, maxn) fac[i] = 1ll * fac[i - 1] * i % Mod;
    ifac[maxn] = fpm(fac[maxn], Mod - 2);
    Fordown (i, maxn - 1, 1) ifac[i] = ifac[i + 1] * (i + 1ll) % Mod;
}

inline int Comb(int n, int m) {
    if (n < 0 || m < 0 || n < m) return 0;
    return 1ll * fac[n] * ifac[m] % Mod * ifac[n - m] % Mod;
}

inline int Calc(int n, int m) {
    return (n < 0 || m < 0) ? 0 : Comb(n + m, m);
}

int n, m;

inline void Flip1(int &x, int &y) { swap(x, y); -- x; ++ y; }

inline void Flip2(int &x, int &y) { swap(x, y); x += m + 2; y -= m + 2; }

int main () {

    File();

    n = read(); m = read();

    Math_Init(n * 2 + m + 1);

    int ans = Calc(n + m + 1, n);
    {
        int x = n + m + 1, y = n;
        while (x >= 0 && y >= 0) {
            Flip1(x, y); (ans -= Calc(x, y)) %= Mod;
            Flip2(x, y); (ans += Calc(x, y)) %= Mod;
        }
    }

    {
        int x = n + m + 1, y = n;
        while (x >= 0 && y >= 0) {
            Flip2(x, y); (ans -= Calc(x, y)) %= Mod;
            Flip1(x, y); (ans += Calc(x, y)) %= Mod;
        }
    }

    ans = (ans + Mod) % Mod;
    printf ("%d\n", ans);

    return 0;

}

「JLOI2015」管道连接

题意

该部门有 \(n\) 个情报站。给出 \(m\) 对情报站 \(u_i, v_i\) 和费用 \(w_i\),表示情报站 \(u_i\)\(v_i\) 之间可以花费 \(w_i\) 建立通道。

如果两个情报站联通那么意味着它们就建立了通道连接。有 \(p\) 个重要情报站,其中每个情报站有一个特定的频道。

现要花费最少的资源,使得任意相同频道的情报站之间都建立通道连接。

\(0 < c_i \leq p \leq 10, \ 0 < u_i, v_i, d_i \leq n \leq 1000, \ 0 \leq m \leq 3000, \ 0 \leq w_i \leq 20000\)

题解

如果学过斯坦纳树不难发现就是裸题了。。。 可是我没学过QAQ

那么就简单讲一下原理与做法。

有一张很大的图,其中有很少的几个关键点。然后要选权值和尽量小的边构成一颗生成树使得这些点都存在于这颗树上,这个就称作 斯坦纳树

如何做呢?考虑 \(dp\) 解决这个问题,令 \(f[i][S]\) 为以 \(i\) 为根节点的树子树中包括了 \(S\) 这个关键点集合的最小代价。

我们一共有两种转移:

  • 对于一个点 \(i\) 它所包含的一个集合 \(S\) ,有转移 \(f_{i, S} = \max_{T \subseteq S} \{f_{i, T} + f_{i, S- T}\}\)

  • 对于每个集合 \(S\) ,把 \(f_{i, S} < \infty\) 的点 \(i\) 放入队列中,跑关于 \(S\) 这个状态的 \(\text{Spfa}\) 。因为此处的 \(dp\) 转移其实就是松弛操作。

然后假设整张图有 \(n\) 个点 \(m\) 条边,有 \(p\) 个关键点。

复杂度是 \(\mathcal O(n \times 3^p + km \times 2^p )\) 。其中 \(k\)\(\text{Spfa}\) 那个系数,可以被卡到 \(\mathcal O(n)\) 。。。


那么对于这道题,我们考虑直接对于每种频道的做一个斯坦纳树。然后令 \(g[S]\)\(S\) 这个集合的最优答案,初值为 \(\min f_{i, S}\) 然后直接做一遍子集 \(dp\) 就好啦。

总结

多学冷门数据结构、算法哇。。。裸题就十分地好做QAQ

代码

#include <bits/stdc++.h>

#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
    freopen ("2110.in", "r", stdin);
    freopen ("2110.out", "w", stdout);
#endif
}

const int N = 1025, M = 3010 * 2, inf = 0x3f3f3f3f;

int n, m, p;

int Head[N], Next[M], to[M], val[M], e = 0;

inline void add_edge(int u, int v, int w) {
    to[++ e] = v; Next[e] = Head[u]; val[e] = w; Head[u] = e;
}

int maxsta;

namespace Steiner_Tree {

    int f[N][N]; bitset<N> inq;

    queue<int> Q;

    void Spfa(int sta) {
        while (!Q.empty()) {
            int u = Q.front(); Q.pop(); inq[u] = false;
            for (int i = Head[u], v = to[i]; i; v = to[i = Next[i]]) {
                if (chkmin(f[v][sta], f[u][sta] + val[i]))
                    if(!inq[v]) Q.push(v), inq[v] = true;
            }
        }
    }

    void Build() {
        For (S, 1, maxsta) {
            For (i, 1, n) {
                for (int T = S - 1; T; T = (T - 1) & S)
                    chkmin(f[i][S], f[i][T] + f[i][S ^ T]);
                if (f[i][S] < inf) Q.push(i), inq[i] = true;
            }
            Spfa(S);
        }
    }

};

int c[N], d[N], g[N], con[N];

inline bool Check(int sta) {
    For (i, 1, p) {
        int cur = sta & con[i];
        if (cur && cur != con[i]) return false;
    }
    return true;
}

int main () {

    File();

    n = read(); m = read(); p = read();

    while (m --) {
        int u = read(), v = read(), w = read();
        add_edge(u, v, w); add_edge(v, u, w);
    }

    For (i, 1, p) c[i] = read(), d[i] = read();

    For (i, 1, p) For (j, 1, p) 
        if (c[i] == c[j]) con[i] |= (1 << (j - 1));
    maxsta = (1 << p) - 1;

    using Steiner_Tree :: f;

    Set(f, 0x3f);
    For (i, 1, p) 
        f[d[i]][1 << (i - 1)] = 0;
    Steiner_Tree :: Build();

    Set(g, 0x3f);
    For (S, 1, maxsta) if (Check(S)) {
        For (i, 1, n) chkmin(g[S], f[i][S]);
        for (int T = S - 1; T; T = (T - 1) & S)
            chkmin(g[S], g[T] + g[S ^ T]);
    }
    printf ("%d\n", g[maxsta]);

    return 0;

}

「JLOI2015」战争调度

题意

王国的公民关系刚好组成一个 \(n\) 层的完全二叉树。公民 \(i\) 的下属是 \(2i\)\(2i +1\)。最下层的公民即叶子节点的公民是平民,平民没有下属,最上层的是国王,中间是各级贵族。

现在这个王国爆发了战争,国王需要决定每一个平民是去种地以供应粮食还是参加战争,每一个贵族(包括国王自己)是去管理后勤还是领兵打仗。一个平民会对他的所有直系上司有贡献度,若一个平民 \(i\) 参加战争,他的某个直系上司 \(j\) 领兵打仗,那么这个平民对上司的作战贡献度为 \(w_{ij}\)。若一个平民 \(i\) 种地,他的某个直系上司 \(j\) 管理后勤,那么这个平民对上司的后勤贡献度为 \(f_{ij}\),若 \(i\)\(j\) 所参加的事务不同,则没有贡献度。为了战争需要保障后勤,国王还要求不多于 \(m\) 个平民参加战争。

国王想要使整个王国所有贵族得到的贡献度最大,请求出这个值。

\(2 \leq n \leq 10, \ m \leq 2^{n-1}, \ 0 \leq w_{ij}, f_{ij} \leq 2000\)

题解

看到完全二叉树,自然要想到关于深度的 \(dp\)

我想到了 \(\text{HNOI2018 D2 T3}\) 那个简单 \(dp\) ,你考虑预留祖先就好了。

具体来说,就是令 \(f_{i, j, S}\) 为到第 \(i\) 个点,选了 \(j\) 个去打仗, \(i\) 的祖先的状态为 \(S\) 的最大贡献度。

然后从前往后依次枚举每个点,考虑相邻两个点的 \(lca\) 深度就行了。

这样的话复杂度是 \(\mathcal O(2^{3(n - 1)})\) ,空间好像开不下,我们滚动第一维就行了。

这样实现了一下,直接就跑过去了??!!好像还挺快的,最慢的要 \(0.1s\) 。。

然后看了看网上题解,发现就是把这个 \(dp\) 在树上记忆化转移就好了,证一证时空复杂度就是 \(O(n 2^{2(n - 1)})\) 的。

QAQ 好像看的那个人的速度还没有我快。。。

总结

树上 \(dp\) 还是要在树上转移卡状态。。别下树 \(dp\) 。。。

代码

#include <bits/stdc++.h>

#define For(i, l, r) for (register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for (register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Rep(i, r) for (register int i = (0), i##end = (int)(r); i < i##end; ++i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl

using namespace std;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

inline int read() {
    int x(0), sgn(1); char ch(getchar());
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') sgn = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * sgn;
}

void File() {
#ifdef zjp_shadow
    freopen ("2111.in", "r", stdin);
    freopen ("2111.out", "w", stdout);
#endif
}

const int N = 1025, B = 513;

int n, m, w[2][N][12], Log2[N], val[2][N][B];

int f[B][B], g[B][B], v[B][B];

int main () {

    File();

    n = read(); m = read();

    int k = 1 << (n - 1);

    Fordown (id, 1, 0) For (i, k, (k << 1) - 1) {
        For (j, 0, n - 2) w[id][i][j] = read();
        Rep (sta, k) Rep (j, n - 1) 
            if ((sta >> j & 1) ^ id) val[id][i][sta] += w[id][i][j];
    }

    Rep (id, 2) Rep (sta, k) f[id][sta] = val[id][k][sta];

    For (i, k + 1, (k << 1) - 1) {
        int x = i - 1, y = i, len = - 1;
        for (; x != y; x >>= 1, y >>= 1) ++ len;

        For (j, 0, min(m, i - k + 1)) Rep (sta, k)
            chkmax(v[j][sta >> len], f[j][sta]);

        Rep (id, 2) For (j, id, min(m, i - k + 1)) Rep (sta, k)
            chkmax(g[j][sta], v[j - id][sta >> len] + val[id][i][sta]);

        For (j, 0, min(m, i - k + 1)) Rep(sta, k)
            f[j][sta] = g[j][sta], g[j][sta] = v[j][sta] = 0;
    }

    int ans = 0;
    For (j, 0, m) Rep(sta, k) chkmax(ans, f[j][sta]);
    printf ("%d\n", ans);

    return 0;

}

JLOI2015 DAY2 简要题解

标签:转移   emc   决定   复杂   +=   滚动   二叉树   题意   ems   

原文地址:https://www.cnblogs.com/zjp-shadow/p/10360330.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!