[题解]CLYZ2018省选训(bao)练(zha)模拟赛 Day 9

本文深入解析了三道经典编程竞赛题目,包括电影座位安排、斐波那契数列的组合计数以及基于树的括号序列问题。通过巧妙的算法设计和数据结构应用,提供了高效解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

找规律大法太好了!

题目

T1:BZOJ 2227 / ZJOI 2011 看电影 (movie)(组合数学+高精)
T2:BZOJ 2660 / BeiJing WC 2012 最多的方案 (count)(找规律+递推)
T3:BZOJ 1095 / ZJOI 2007 捉迷藏 (hide)(树的括号序列+线段树)
今天难题场啊,能AK的人是神。——from cyx

T1

分析

如果K<NK<N,则所有方案都是不行的,输出0101
总方案数显然为KNKN。怎样构造一个合法方案呢?
考虑将第KK个座位和第1个座位连接起来,形成一个环,越过第KK个座位的人就回到第1个座位。那么合法方案就是不越过第KK个座位的方案数。
如何求不越过分界点的方案数呢?
再加一个座位,共K+1个座位,同样把第KK个座位和第1个座位连接起来,形成一个环。这样分配的方案数是(K+1)N(K+1)N。由于是环,所以去除重复的方案,为(K+1)N1(K+1)N−1
可以看出,空的座位一定没有人越过。所以选一个空座位断开,形成一个序列,就构造成了一个合法方案。
一共有K+1NK+1−N个空位,所以答案为:

(K+1)N1(K+1N)KN(K+1)N−1(K+1−N)KN

由于需要用到高精,要求最简分数,分子又是幂的形式,所以可以将K+1K+1K+1NK+1−NKK分别分解质因数,这样就能较为方便地进行约分。

Source

#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;
}
typedef long long ll;
const int N = 205, M = 6005, MX = 1e6;
int n, K, cnt1[N], cnt2[N];
struct cyx {
    int n; ll a[M];
    cyx() {}
    cyx(int _n, int val) :
        n(_n) {memset(a, 0, sizeof(a)); a[1] = val;}
    friend inline cyx operator * (cyx a, cyx b) {
        int i, j; cyx res = cyx(a.n + b.n + 2, 0);
        for (i = 1; i <= a.n; i++) for (j = 1; j <= b.n; j++)
            res.a[i + j - 1] += a.a[i] * b.a[j];
        for (i = 1; i < res.n; i++)
            res.a[i + 1] += res.a[i] / MX, res.a[i] %= MX;
        while (res.n > 1 && !res.a[res.n]) res.n--; return res;
    }
    friend inline cyx operator ^ (cyx a, int b) {
        cyx res = cyx(1, 1);
        while (b) {
            if (b & 1) res = res * a;
            a = a * a;
            b >>= 1;
        }
        return res;
    }
};
void pri(int a, int b, bool op) {
    int i, S = sqrt(a), tmp = a, *cnt = op ? cnt1 : cnt2;
    for (i = 2; i <= S; i++) while (tmp % i == 0) cnt[i] += b, tmp /= i;
    if (tmp > 1) cnt[tmp] += b;
}
void work() {
    memset(cnt1, 0, sizeof(cnt1)); memset(cnt2, 0, sizeof(cnt2));
    int i; n = read(); K = read(); if (n > K) return (void) puts("0 1");
    pri(K + 1, n - 1, 1); pri(K - n + 1, 1, 1); pri(K, n, 0);
    for (i = 1; i <= 200; i++) {
        int tmp = min(cnt1[i], cnt2[i]); cnt1[i] -= tmp; cnt2[i] -= tmp;
    }
    cyx r1 = cyx(1, 1), r2 = cyx(1, 1);
    for (i = 1; i <= 200; i++) r1 = r1 * (cyx(1, i) ^ cnt1[i]);
    for (i = 1; i <= 200; i++) r2 = r2 * (cyx(1, i) ^ cnt2[i]);
    printf("%d", r1.a[r1.n]); for (i = r1.n - 1; i; i--)
        printf("%06d", r1.a[i]);
    printf(" %d", r2.a[r2.n]); for (i = r2.n - 1; i; i--)
        printf("%06d", r2.a[i]); printf("\n");
}
int main() {
    int i, j, T = read();
    while (T--) work();
    return 0;
}

T2

分析

先打个爆搜找个规律,可以发现,如果找到了一种包含斐波那契数个数最少的方案,那么其他的方案可以由这个个数最少的方案拆分而成。
如样例中,包含个数最少的方案是16=3+13(其他的方案都是33个数,仅有这一个是2个数),
而剩下的拆分方案中,有5+8=135+8=131+2=31+2=3,都可以由包含个数最少的方案拆分而成。
回到原题,发现[1,1018][1,1018]内的斐波那契数很少,直接列出来。
然后用贪心构造出包含个数最少的方案,也就是从大数往小的扫描,如果能用就用上。设构造出的方案包含mm个数,分别为Fa1Fa2Fa2,…,FamFam,且aa是递增的。
DP,定义状态:
f[i][0/1]表示到了第i(1im)i(1≤i≤m)个数。第二维为00表示Fai这个数没有被用上,否则被用上。
f[i][1]f[i][1]的转移比较简单:

f[i][1]=f[i1][0]+f[i1][1]f[i][1]=f[i−1][0]+f[i−1][1]

这种情况下因为aa是递增的,因此前面无论怎么拆分都不会拆分出Fai
f[i][0]f[i][0]的转移:
f[i][0]=aiai12×f[i1][0]+aiai112×f[i1][1]f[i][0]=⌊ai−ai−12⌋×f[i−1][0]+⌊ai−ai−1−12⌋×f[i−1][1]

也就是说,FaiFai要被拆分成多个(至少22个)斐波那契数的和。
当拆分方案中包含Fai1时,FaiFai的拆分出的任何数都不能小于等于Fai1Fai−1,当拆分方案中不包含Fai1Fai−1时,就一定包含Fai11Fai−1−1(为什么呢?打表啊!找规律啊!),这时FaiFai拆分出的任何数都不能小于等于Fai11Fai−1−1
至于aiai12⌊ai−ai−12⌋aiai112⌊ai−ai−1−12⌋这两个系数,还是找规律吧,xyz32768蒟蒻第一次认识到找规律居然有那么多妙用。。。。。。

Source

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll; const int N = 205;
ll a, f[N], F[N][2]; int n, tot, tmp[N], id[N];
int cxd(int i, int j) {return i - j >> 1;}
int main() {
    int i; cin >> a; f[1] = 1; f[2] = 2;
    for (n = 3; f[n - 2] + f[n - 1] <= a; n++)
        f[n] = f[n - 2] + f[n - 1]; n--;
    for (i = n; i; i--) if (f[i] <= a) tmp[++tot] = i, a -= f[i];
    for (i = tot; i; i--) id[tot - i + 1] = tmp[i];
    F[1][1] = 1; F[1][0] = id[1] - 1 >> 1;
    for (i = 2; i <= tot; i++) {
        F[i][1] = F[i - 1][0] + F[i - 1][1];
        F[i][0] = F[i - 1][0] * cxd(id[i], id[i - 1]) +
            F[i - 1][1] * cxd(id[i], id[i - 1] + 1);
    }
    cout << F[tot][0] + F[tot][1] << endl;
    return 0;
}

T3

分析

动态树分治什么的不会,只好写了线段树QWQ
一棵树括号序列的定义:
对树进行一次dfs,能求出一棵树的括号序列,一对匹配的括号表示一棵子树,括号内的第一个元素表示子树的根,一个左括号表示进入dfs(向下走),一个右括号表示退出dfs(向上走)。
如样例的树以11为根,括号序列为:(1(2(3(4)(5)(6(7)(8)))))。
假如要查询266的距离,就可以提取出266之间的括号:(()()(
除去匹配的括号,得到:((
一共2个括号,所以226之间的距离为22
显然对于所有的点对u,v,提取括号序列中uuv之间的括号,去除匹配括号后,一定是aa个右括号+b个左括号的形式。
所以就是要动态维护max{a+b}max{a+b},考虑线段树。
每个节点上记录三个值:
aa:对应区间内,去除匹配括号后,右括号的个数。
b:对应区间内,去除匹配括号后,左括号的个数。
disdis:对应区间内的max{a+b}max{a+b}
现在就考虑如何合并信息(下面设lclcrcrc分别为pp的左右子节点)。
1、当blcarc时,rcrc的右括号全部被匹配掉了。
所以这时候ap=alc,bp=blcarc+brcap=alc,bp=blc−arc+brc
2、当blc<arcblc<arc时,lclc的左括号全部被匹配掉了。
所以这时候ap=alc+arcblc,bp=brcap=alc+arc−blc,bp=brc
综合起来,

ap=alc+max(0,arcblc)ap=alc+max(0,arc−blc)

bp=brc+max(0,blcarc)bp=brc+max(0,blc−arc)

再考虑dispdisp,发现dispdisp不仅仅能从dislcdislcdisrcdisrc转移过来,还能从lclc的后缀和rcrc的前缀合并而成。
所以还要记上四个信息:
rpprppmax{a+b|max{a+b|对应子串是pp的一个后缀,且子串的一个元素是黑点}
rmprmpmax{ab|max{a−b|对应子串是pp的一个后缀,且子串的一个元素是黑点}
lpplppmax{a+b|max{a+b|对应子串是pp的一个前缀,且子串的一个元素是黑点}
lmplmpmax{ba|max{b−a|对应子串是pp的一个前缀,且子串的一个元素是黑点}
从子节点更新的代码:
void upt(int p) {
    A[p] = A[p2] + max(0, A[p3] - B[p2]); B[p] = B[p3] + max(0, B[p2] - A[p3]);
    dis[p] = max(max(dis[p2], dis[p3]), max(rp[p2] + lm[p3], rm[p2] + lp[p3]));
    rp[p] = max(max(rp[p2] - A[p3] + B[p3], rm[p2] + A[p3] + B[p3]), rp[p3]);
    rm[p] = max(rm[p2] + A[p3] - B[p3], rm[p3]);
    lp[p] = max(max(lm[p3] + A[p2] + B[p2], lp[p3] + A[p2] - B[p2]), lp[p2]);
    lm[p] = max(lm[p3] + B[p2] - A[p2], lm[p2]);
}

Source

#include <cmath>
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define p2 p << 1
#define p3 p << 1 | 1
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;
}
inline char get() {
    char c; while ((c = getchar()) != 'C' && c != 'G'); return c;
}
const int N = 1e5 + 5, M = N * 3, L = M << 2;
int n, m, ecnt, nxt[M], adj[N], go[M], que[M], T, A[L], B[L], dis[L],
rp[L], rm[L], lp[L], lm[L], cnt, whi[M]; bool clo[N];
void add_edge(int u, int v) {
    nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
    nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}
void dfs(int u, int fu) {
    que[++T] = -1; whi[que[++T] = u] = T;
    for (int e = adj[u], v; e; e = nxt[e])
        if ((v = go[e]) != fu) dfs(v, u); que[++T] = 0;
}
void upt(int p) {
    A[p] = A[p2] + max(0, A[p3] - B[p2]); B[p] = B[p3] + max(0, B[p2] - A[p3]);
    dis[p] = max(max(dis[p2], dis[p3]), max(rp[p2] + lm[p3], rm[p2] + lp[p3]));
    rp[p] = max(max(rp[p2] - A[p3] + B[p3], rm[p2] + A[p3] + B[p3]), rp[p3]);
    rm[p] = max(rm[p2] + A[p3] - B[p3], rm[p3]);
    lp[p] = max(max(lm[p3] + A[p2] + B[p2], lp[p3] + A[p2] - B[p2]), lp[p2]);
    lm[p] = max(lm[p3] + B[p2] - A[p2], lm[p2]);
}
void orz(int p, int pos) {
    A[p] = que[pos] == 0; B[p] = que[pos] == -1; dis[p] = -0x3f3f3f3f;
    rp[p] = rm[p] = lp[p] = lm[p] =
        que[pos] > 0 && clo[que[pos]] ? 0 : -0x3f3f3f3f;
}
void build(int l, int r, int p) {
    if (l == r) return orz(p, l); int mid = l + r >> 1;
    build(l, mid, p2); build(mid + 1, r, p3); upt(p);
}
void change(int l, int r, int pos, int p) {
    if (l == r) {
        if (clo[que[pos]]) cnt--; else cnt++; clo[que[pos]] ^= 1; orz(p, pos);
        return;
    };
    int mid = l + r >> 1;
    if (pos <= mid) change(l, mid, pos, p2);
    else change(mid + 1, r, pos, p3); upt(p);
}
int main() {
    memset(clo, true, sizeof(clo));
    int i, x, y; cnt = n = read(); for (i = 1; i < n; i++)
        x = read(), y = read(), add_edge(x, y); dfs(1, 0); build(1, T, 1);
    char op; m = read(); while (m--) {
        op = get(); if (op == 'C') x = read(), change(1, T, whi[x], 1);
        else {
            if (cnt == 0) puts("-1"); else if (cnt == 1) puts("0");
            else printf("%d\n", dis[1]);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值