找规律大法太好了!
题目
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个座位和第个座位连接起来,形成一个环,越过第KK个座位的人就回到第个座位。那么合法方案就是不越过第KK个座位的方案数。
如何求不越过分界点的方案数呢?
再加一个座位,共个座位,同样把第KK个座位和第个座位连接起来,形成一个环。这样分配的方案数是(K+1)N(K+1)N。由于是环,所以去除重复的方案,为(K+1)N−1(K+1)N−1。
可以看出,空的座位一定没有人越过。所以选一个空座位断开,形成一个序列,就构造成了一个合法方案。
一共有K+1−NK+1−N个空位,所以答案为:
由于需要用到高精,要求最简分数,分子又是幂的形式,所以可以将K+1K+1,K+1−NK+1−N和KK分别分解质因数,这样就能较为方便地进行约分。
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
分析
先打个爆搜找个规律,可以发现,如果找到了一种包含斐波那契数个数最少的方案,那么其他的方案可以由这个个数最少的方案拆分而成。
如样例中,包含个数最少的方案是(其他的方案都是33个数,仅有这一个是个数),
而剩下的拆分方案中,有5+8=135+8=13,1+2=31+2=3,都可以由包含个数最少的方案拆分而成。
回到原题,发现[1,1018][1,1018]内的斐波那契数很少,直接列出来。
然后用贪心构造出包含个数最少的方案,也就是从大数往小的扫描,如果能用就用上。设构造出的方案包含mm个数,分别为,Fa2Fa2,…,FamFam,且aa是递增的。
DP,定义状态:
表示到了第i(1≤i≤m)i(1≤i≤m)个数。第二维为00表示这个数没有被用上,否则有被用上。
f[i][1]f[i][1]的转移比较简单:
这种情况下因为aa是递增的,因此前面无论怎么拆分都不会拆分出。
f[i][0]f[i][0]的转移:
也就是说,FaiFai要被拆分成多个(至少22个)斐波那契数的和。
当拆分方案中包含时,FaiFai的拆分出的任何数都不能小于等于Fai−1Fai−1,当拆分方案中不包含Fai−1Fai−1时,就一定包含Fai−1−1Fai−1−1
至于⌊ai−ai−12⌋⌊ai−ai−12⌋和⌊ai−ai−1−12⌋⌊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)))))。
假如要查询到66的距离,就可以提取出与66之间的括号:(()()(
除去匹配的括号,得到:((
一共个括号,所以22到之间的距离为22。
显然对于所有的点对,提取括号序列中uu到之间的括号,去除匹配括号后,一定是aa个右括号个左括号的形式。
所以就是要动态维护max{a+b}max{a+b},考虑线段树。
每个节点上记录三个值:
aa:对应区间内,去除匹配括号后,右括号的个数。
:对应区间内,去除匹配括号后,左括号的个数。
disdis:对应区间内的max{a+b}max{a+b}。
现在就考虑如何合并信息(下面设lclc和rcrc分别为pp的左右子节点)。
1、当时,rcrc的右括号全部被匹配掉了。
所以这时候ap=alc,bp=blc−arc+brcap=alc,bp=blc−arc+brc。
2、当blc<arcblc<arc时,lclc的左括号全部被匹配掉了。
所以这时候ap=alc+arc−blc,bp=brcap=alc+arc−blc,bp=brc。
综合起来,
再考虑dispdisp,发现dispdisp不仅仅能从dislcdislc和disrcdisrc转移过来,还能从lclc的后缀和rcrc的前缀合并而成。
所以还要记上四个信息:
rpprpp:max{a+b|max{a+b|对应子串是pp的一个后缀,且子串的前一个元素是黑点
rmprmp:max{a−b|max{a−b|对应子串是pp的一个后缀,且子串的前一个元素是黑点
lpplpp:max{a+b|max{a+b|对应子串是pp的一个前缀,且子串的后一个元素是黑点
lmplmp:max{b−a|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;
}