[题解]NOIP2016提高组の题解集合 - by xyz32768

NOIP2016提高组的难度参差不齐……按照难度从小到大排序讲吧……

Top 6:D1T1 玩具谜题(toy) - 模拟

模拟题。根据小人的朝向和向左/右数来判断走向。注意边界。

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
const int N = 1e5 + 5;
int n, m, v[N]; string s[N];
int main() {
    //freopen("toy.in", "r", stdin);
    //freopen("toy.out", "w", stdout);
    int i, now = 0; n = read(); m = read();
    for (i = 1; i <= n; i++) v[i] = read(), cin >> s[i];
    for (i = 1; i <= m; i++)
        if (v[now + 1] ^ read()) now = (now + read()) % n;
        else now = (now - read() + n) % n;
    cout << s[now + 1] << endl;
    return 0;
}

Top 5:D2T1 组合数问题(problem) - 组合数+前缀和

首先使用公式 Cmn=Cmn1+Cm1n1 预处理 2000 以内的组合数取模 k 的值,判断一下每个值是否为0后再求一下前缀和,就能 O(1) 一组询问了。
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
const int N = 2000;
int m, n, PYZ, c[N + 5][N + 5], col[N + 5][N + 5], cnt[N + 5][N + 5];
int main() {
    //freopen("problem.in", "r", stdin);
    //freopen("problem.out", "w", stdout);
    int i, j, T; T = read(); PYZ = read();
    for (i = 0; i <= N; i++) c[i][0] = 1;
    for (i = 1; i <= N; i++) for (j = 1; j <= i; j++)
        c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % PYZ;
    for (j = 1; j <= N; j++) for (i = 1; i <= (N - j + 1); i++)
        col[j][i] = col[j][i - 1] + (c[i + j - 1][j] == 0);
    for (i = 1; i <= N; i++) for (j = 1; j <= i; j++)
        cnt[i][j] = cnt[i][j - 1] + col[j][i - j + 1];
    while (T--) {
        m = read(); n = read();
        printf("%d\n", cnt[m][min(m, n)]);
    }
    return 0;
}

Top 4:D2T3 愤怒的小鸟(angrybirds) - 状压DP

首先知道,三个不在一条直线上的点可以确定一条抛物线。而 (0,0) 是已经固定的点,所以只要枚举两个点,就可以得到一条抛物线。
考虑状态压缩。先预处理出 con[i][j] ,表示第 i 只猪和第j只猪与 (0,0) 组成的抛物线,储存该抛物线能消灭猪的集合(用 2 进制表示,一位为1表示不能消灭这个猪,为 0 则可以消灭这个猪)。
预处理较简单,注意两个猪(x1,y1)(x2,y2)组成的抛物线的解析式为:
y=x1y2x2y1x1x2(x2x1)x2+x22y1x21y2x1x2(x2x1)x
然后设 f[S] 为当前猪的状态为二进制 S (为1则未消灭,为 0 则已消灭)。边界即f[0]=0
转移有两种:
1、 f[S]=min(f[S],f[S & con[i][j]]+1),i,jS,ij
2、 f[S]=min(f[S],f[S2i1]+1),iS
这样就能 O(2nn2) 解决了。
优化:每一个猪是肯定要消灭的,所以可以在第一个转移中,用 S 集合中的第一个数来代替i,将复杂度优化到 O(2nn)
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
const int N = 20; const double eps = 1e-9;
int n, f[1 << N], ef[N][N];
double X[N], Y[N];
struct cyx {
    double a, b;
    cyx() {}
    cyx(double _a, double _b) :
        a(_a), b(_b) {}
};
cyx quad(double x1, double y1, double x2, double y2) {
    if (abs(x1 * x2 * (x2 - x1)) < eps) return cyx(1e20, 1e20);
    double a = (x1 * y2 - x2 * y1) / (x1 * x2 * (x2 - x1)),
        b = (y1 * x2 * x2 - y2 * x1 * x1) / (x1 * x2 * (x2 - x1));
    return a < -eps ? cyx(a, b) : cyx(1e20, 1e20);
}
int Dp(int state) {
    if (f[state] != -1) return f[state];
    if (!state) return 0;
    int i, fir, res = n; for (i = 1; i <= n; i++)
        if ((state >> i - 1) & 1)
            {fir = i; break;}
    for (i = 1; i <= n; i++)
        if (((state >> i - 1) & 1)) {
            if (i != fir && (state & ef[fir][i]) != state)
                res = min(res, Dp(state & ef[fir][i]) + 1);
            res = min(res, Dp(state ^ (1 << i - 1)) + 1);
        }
    return f[state] = res;
}
void work() {
    int i, j, k; n = read(); read(); memset(f, -1, sizeof(f));
    for (i = 1; i <= n; i++) scanf("%lf%lf", &X[i], &Y[i]);
    for (i = 1; i <= n; i++) for (j = i + 1; j <= n; j++)
        ef[i][j] = (1 << n) - 1;
    for (i = 1; i <= n; i++) for (j = i + 1; j <= n; j++) {
        cyx q = quad(X[i], Y[i], X[j], Y[j]);
        if (q.a > 1e19) continue;
        for (k = 1; k <= n; k++)
            if (abs(q.a * X[k] * X[k] + q.b * X[k] - Y[k]) < eps)
                ef[i][j] ^= 1 << k - 1;
    }
    printf("%d\n", Dp((1 << n) - 1));
}
int main() {
    //freopen("angrybirds.in", "r", stdin);
    //freopen("angrybirds.out", "w", stdout);
    int T = read();
    while (T--) work();
    return 0;
}

Top 3:D2T2 蚯蚓(earthworm) - 队列

容易想到用堆维护蚯蚓长度,对于蚯蚓的增长,就用一个 delta 标记。
但是 O(nlogn) 的复杂度无法承受 n7106 的数据范围。
q=0 的时候,可以很容易得到每次切蚯蚓得到的两段是单调不增的。
q0 时也满足这个性质。
证明:
如果说这一次切掉的蚯蚓实际长度 x+delta ,下一次切掉的蚯蚓实际长度 y+delta+q ,由题意容易得到 x>y
那么这一次把蚯蚓切成的两段中,第一段的相对长度 p(x+delta)deltaq=px+pdeltadeltaq
下一次把蚯蚓切成的两段中,第一段相对长度 p(y+delta+q)delta2q=py+pdelta+pqdelta2q
0<p<1,q>=0 pqq<0
又由于 x>y ,所以 p(xy)>0 ,即 pxpy>pqq
也就是 px+pdeltadeltaq>py+pdelta+pqdelta2q 。第二段同理。即每次切掉的两个蚯蚓长度是单调不增的。
根据这个特性,可以维护 3 个队列,分别为未切的蚯蚓,第一段和第二段。每一次操作就在3个队列的队首中选最大值出队,切完后加入到第二个队列和第三个队列的队尾,同时维护 delta 。输出答案后,将三个队列中剩余的元素归并即可。
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
const int N = 1e5 + 5, M = 7e6 + 5, INF = 0x3f3f3f3f;
int n, m, q, u, v, t, a[N], Q[4][M + N], H[4], T[4], add;
double p;
int pop() {
    int i, id = 0, ans = -INF;
    for (i = 1; i <= 3; i++)
        if (H[i] < T[i] && Q[i][H[i] + 1] > ans)
            id = i, ans = Q[id][H[id] + 1];
    return Q[id][++H[id]];
} 
int main() {
    //freopen("earthworm.in", "r", stdin);
    //freopen("earthworm.out", "w", stdout);
    int i, cnt = 0; n = read(); m = read(); q = read();
    u = read(); v = read(); t = read(); p = (1.0 * u) / (1.0 * v);
    for (i = 1; i <= n; i++) a[i] = read();
    sort(a + 1, a + n + 1); for (i = n; i >= 1; i--)
        Q[1][++T[1]] = a[i];
    for (i = 1; i <= m; i++) {
        int x = pop() + add, y = (int) (1.0 * x * p), z = x - y;
        add += q; y -= add; z -= add;
        Q[2][++T[2]] = y; Q[3][++T[3]] = z;
        if ((++cnt) == t) printf("%d ", x), cnt = 0;
    }
    printf("\n"); cnt = 0;
    for (i = 1; i <= n + m; i++) {
        int x = pop() + add;
        if ((++cnt) == t) printf("%d ", x), cnt = 0;
    }
    printf("\n");
    return 0;
}

Top 2:D1T3 换教室(classroom) - Floyd+期望DP

首先使用Floyd求出任意两点之间的最短路,设 i j的最短路为 len[i][j]
DP模型:设 f[i][j][0/1] 为到了第 i 个时间段,换了j次教室,第三维为 0 表示不换第i个教室,否则表示换第 i 个教室。
转移有4种:
1、第 i1 和第 i 个教室都不换。即
f[i][j][0]=min(f[i][j][0],f[i1][j][0]+len[ci1][ci])
2、换第 i1 个教室,不换第 i 个教室。这时候将答案分成两部分,一部分是1 i1 ,第二部分是 i1 i
这时候第一部分的期望为f[i1][j][1],第二部分的期望为 len[di1][ci]ki1+len[ci1][ci](1ki1) 。所以:
f[i][j][0]=min(f[i][j][0],f[i1][j][1]+len[di1][ci]ki1+len[ci1][ci](1ki1)
3、不换第 i1 个教室,换第 i 个教室。这时候也是一样的思想,即
f[i][j][1]=min(f[i][j][1],f[i1][j1][0]+len[ci1][di]ki+len[ci1][ci](1ki))
4、第 i1 个教室和第 i 个教室都换。这时候是情况最多的,有4种。分别为:
(1)两个申请都通过。即 len[di1][di] ,发生的概率为 ki1ki
(2)只通过第一个申请。即 len[di1][ci] ,发生的概率为 ki1(1ki)
(3)只通过第二个申请。即 len[ci1][di] ,发生的概率为 (1ki1)ki
(4)两个申请都不通过。即 len[ci1][ci] ,发生的概率为 (1ki1)(1ki)
得出转移:
f[i][j][1]=min(f[i1][j1][1],len[di1][di]ki1ki
+len[di1][ci]ki1(1ki)+len[ci1][di](1ki1)ki
+len[ci1][ci](1ki1)(1ki))
代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
void chkmin(double &a, double b) {
    a = min(a, b);
}
const int N = 2005, M = 305, INF = 0x3f3f3f3f;
int n, m, v, e, c[N], d[N], ex[M][M];
double K[N], f[N][N][2];
void floyd() {
    int i, j, k; for (k = 1; k <= v; k++)
    for (i = 1; i <= v; i++) for (j = 1; j <= v; j++)
        ex[i][j] = min(ex[i][j], ex[i][k] + ex[k][j]);
    for (i = 1; i <= v; i++) ex[i][i] = 0;
}
double solve() {
    floyd(); int i, j;
    for (i = 1; i <= n; i++) for (j = 0; j <= m; j++)
        f[i][j][0] = f[i][j][1] = 1e20;
    f[1][0][0] = f[1][1][1] = 0;
    for (i = 2; i <= n; i++) for (j = 0; j <= min(i, m); j++) {
        chkmin(f[i][j][0], f[i - 1][j][0] +
            ex[c[i - 1]][c[i]]);
        if (j == 0) continue;
        chkmin(f[i][j][0], f[i - 1][j][1] +
        1.0 * ex[d[i - 1]][c[i]] * K[i - 1]
        + 1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i - 1]));
        chkmin(f[i][j][1], f[i - 1][j - 1][0] +
        1.0 * ex[c[i - 1]][d[i]] * K[i]
        + 1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i]));
        chkmin(f[i][j][1], f[i - 1][j - 1][1] +
        1.0 * ex[d[i - 1]][d[i]] * K[i - 1] * K[i] +
        1.0 * ex[c[i - 1]][d[i]] * (1.0 - K[i - 1]) * K[i] +
        1.0 * ex[d[i - 1]][c[i]] * K[i - 1] * (1.0 - K[i]) +
        1.0 * ex[c[i - 1]][c[i]] * (1.0 - K[i - 1]) * (1.0 - K[i]));
    }
    double res = 1e20;
    for (j = 0; j <= min(n, m); j++)
        res = min(res, min(f[n][j][0], f[n][j][1]));
    return res;
}
int main() {
    //freopen("classroom.in", "r", stdin);
    //freopen("classroom.out", "w", stdout);
    int i, x, y, z; memset(ex, INF, sizeof(ex));
    n = read(); m = read(); v = read(); e = read();
    for (i = 1; i <= n; i++) c[i] = read();
    for (i = 1; i <= n; i++) d[i] = read();
    for (i = 1; i <= n; i++) scanf("%lf", &K[i]);
    while (e--) {
        x = read(); y = read(); z = read();
        if (x != y) ex[x][y] = ex[y][x] = min(ex[x][y], z);
    }
    printf("%.2lf\n", solve());
    return 0;
}

Top 1:D1T2 天天爱跑步(running) - dfs+统计

这题是2016中最难的。首先,还是把原图视为有根树,求出每个 Si Ti 的lca即 lcai
可以发现,第 u 个观察员能观察到的玩家数目为:
Si lcai 的路径上合法的 i 的个数+ lcai Ti 的路径上合法的 i 个数,如果lcai算了两遍则减 1
怎么求前两个内容呢?
先考虑第一个,设u的深度为 dep[u] ,那么对于一个点 v 往上走,点v在恰好 Wu 的时间经过 u 的条件是dep[u]+Wu=dep[v]。但是可以发现,对于任意一个玩家 i Si一旦走到了 lcai 就不再往上走了。所以这时候的结果就是所有满足 dep[u]+Wu=dep[Si] 并且 lcai 不在 u 的子树内(不包括u), Si u 的子树内(包括u)的 i 个数。
第二个内容也是差不多的思想。对于一个玩家i,在部分路径 lcai Ti 中恰好第 Wu 秒经过点 u 的条件是dep[Si]+dep[u]2dep[lcai]=Wu,移项得 dep[u]Wu=2dep[lcai]dep[Si] 。同样, Si 一旦走到了 lcai 就不再往上走。所以求的就是满足 dep[u]Wu=2dep[lcai]dep[Si] 并且 lcai 不在 u 的子树内(不包括u), Ti u 的子树内(包括u)的 i 个数。
考虑怎样实现。对于第一个内容,容易想到用一个计数器cnt[x]来维护到当前考虑过的所有 Si 中,满足 dep[Si]=x i 的个数。但是现在只想统计当前子树内的Si,怎么办呢?可以发现,对于每一个节点,只需要去考虑深度为 dep[u]+Wu 的节点。所以,使用一次dfs来解决这个问题,搜到一个节点的时候 u ,先记下当前的cnt[dep[u]+Wu],然后再去搜 u 的子节点,求出cnt[dep[u]+Wu]相对于搜 u 的所有子节点之前的变化量(增量),就是u的子树内满足 dep[u]+Wu=dep[Si] i 的个数。再考虑消除lcai的影响。因为如果 lcai u 的子树内(不包括u),那么 lcai 一定也在 u 的父亲的子树内,所以对于这一部分的答案,直接从子节点传上来即可。但在子节点v的结果往父亲 u 上传时,如果v是某些 lcai ,那么传到 u 时就要消除因lca造成的影响,也就是说,对于所有 dep[v]+Wv=dep[Si] 并且 lcai=v i ,在传到u时必须去掉。解决办法就是维护一个vector或邻接表,储存 v 作为哪些玩家路径的lca,这样就能方便地往上传了。
第二个内容的实现也是差不多,也是用dfs和增量维护子树内的信息,用vector或邻接表进行去重。但是注意细节: dep[u]Wu 有可能是负数,因此统计数组的下标要加上一个定值。
复杂度:如果用倍增求lca,那么复杂度为 O(nlogn) ,如果用Tarjan离线求lca,那么复杂度为 O(n)
代码:

#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res;
}
const int N = 3e5 + 5, LogN = 22, M = 3e5;
int n, m, ecnt, nxt[N << 1], adj[N], go[N << 1], tim[N], S[N], T[N],
L[N], dep[N], fa[N][LogN], up[N], down[N], res[N], cnt[N << 1],
cnt_s[N], cnt_t[N], len[N], md;
vector<int> lca_s[N], lca_t[N], s_t[N];
void add_edge(int u, int v) {
    nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
}
void dfs(int u, int fu) {
    dep[u] = dep[fu] + 1; fa[u][0] = fu;
    md = max(md, dep[u]);
    int i; for (i = 1; i <= 20; i++)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for (int e = adj[u], v; e; e = nxt[e])
        if ((v = go[e]) != fu) dfs(v, u);
}
int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    int i; for (i = 20; i >= 0; i--) {
        if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
        if (u == v) return u;
    }
    for (i = 20; i >= 0; i--)
        if (fa[u][i] != fa[v][i]) {
            u = fa[u][i];
            v = fa[v][i];
        }
    return fa[u][0];
}
void dfs1(int u, int fu) {
    int i, cc = lca_s[u].size(), tot = cnt[up[u]];
    for (int e = adj[u], v; e; e = nxt[e])
        if ((v = go[e]) != fu)
            dfs1(v, u);
    cnt[dep[u]] += cnt_s[u];
    if (up[u] <= md) res[u] += cnt[up[u]] - tot;
    for (i = 0; i < cc; i++) cnt[dep[lca_s[u][i]]]--;
}
void dfs2(int u, int fu) {
    int i, cc = lca_t[u].size(), ff = s_t[u].size(), tot = cnt[down[u] + M];
    for (int e = adj[u], v; e; e = nxt[e])
        if ((v = go[e]) != fu)
            dfs2(v, u);
    for (i = 0; i < ff; i++) cnt[s_t[u][i] + M]++;
    res[u] += cnt[down[u] + M] - tot;
    for (i = 0; i < cc; i++) cnt[lca_t[u][i] + M]--;
}
int main() {
    //freopen("running.in", "r", stdin);
    //freopen("running.out", "w", stdout);
    int i, x, y; n = read(); m = read();
    for (i = 1; i < n; i++) {
        x = read(); y = read();
        add_edge(x, y); add_edge(y, x);
    }
    dfs(1, 0); for (i = 1; i <= n; i++)
        tim[i] = read(), up[i] = dep[i] + tim[i], down[i] = dep[i] - tim[i];
    for (i = 1; i <= m; i++) S[i] = read(), T[i] = read(),
        L[i] = lca(S[i], T[i]), cnt_s[S[i]]++, cnt_t[T[i]]++,
        len[i] = dep[S[i]] + dep[T[i]] - (dep[L[i]] << 1),
        lca_s[L[i]].push_back(S[i]), lca_t[L[i]].push_back(dep[T[i]] - len[i]),
        s_t[T[i]].push_back(dep[T[i]] - len[i]);
    dfs1(1, 0); memset(cnt, 0, sizeof(cnt)); dfs2(1, 0);
    for (i = 1; i <= m; i++) if (dep[S[i]] - dep[L[i]] == tim[L[i]])
        res[L[i]]--;
    for (i = 1; i <= n; i++) printf("%d ", res[i]); printf("\n");
    return 0;
}

总结

蒟蒻写完这份题解的之后的第7天,NOIP2017也要开始了。祝各位神犇大佬AK NOIP,AK 省选,AK NOI,AK CTSC,AK IOI,AK ACM系列各大比赛!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值