UOJ Logo oi_loser的博客

博客

正睿NOIP普及组模拟赛第5场题解

2017-10-18 10:57:03 By oi_loser

由于本人不是这场的出题人,题解和标算可能不一致。


Igra

关键字:贪心

记 $A,B$ 分别为Alice和Bob的串。问题等价于将 $a$ 个 $\mathrm{a}$,$b$ 个 $\mathrm{b}$,$c$ 个 $\mathrm{c}$ 填入串 $A$,使得 $A[i]=B[i]$ 恒成立。

由于数据保证有解,考虑这样一种思路:从左往右贪心确定 $A$ 的每个字符的最小值。

依次对于 $i=1,2,\cdots,n$,枚举最小的 $A[i]$,要求这样的 $A[i]$ 能使问题有解(注意 $A[1,i-1]$ 已经确定)。用 $a,b,c$ 分别表示字母 $\mathrm{a},\mathrm{b},\mathrm{c}$ 的剩余个数,不妨以 $A[i]=\mathrm{a}$ 为例,则需要满足的条件是:

  • $a > 0$ 且 $B[i]\ne\mathrm{a}$;
  • 可以将 $a-1$ 个 $\mathrm{a}$,$b$ 个 $\mathrm{b}$,$c$ 个 $\mathrm{c}$ 填入 $A[i+1,n]$,使得 $A[j]\ne B[j]$ 恒成立。

如果前者不成立,那么显然 $A[i]=\mathrm{a}$ 不可行。现在我们假设前者成立,关键在于判定后者是否成立。先把 $a$ 减掉 $1$,并且设 $B[i+1,n]$ 中,$\mathrm{a},\mathrm{b},\mathrm{c}$ 出现的个数分别 $a',b',c'$(显然 $a'+b'+c'=a+b+c$),$f(\alpha,\beta)$ 表示 $A$ 中字母 $\alpha$ 有多少个被放在了 $B$ 中字母为 $\beta$ 的位置(即满足 $A[j]=\alpha,B[j]=\beta$ 的位置 $j$ 的个数),则有:

--------+-------------+-------------+-------------
 f(α,β) |    β="a"    |    β="b"    |    β="c"    
--------+-------------+-------------+-------------
  α="a" |      0      |     (1)     |     (2)       (1)+(2)=a
--------+-------------+-------------+-------------
  α="b" |     (3)     |      0      |     (4)       (3)+(4)=b
--------+-------------+-------------+-------------
  α="c" |     (5)     |     (6)     |      0        (5)+(6)=c
--------+-------------+-------------+-------------
          (3)+(5)=a'    (1)+(6)=b'    (2)+(4)=c'

设 (1) 为 $x$,可以推出

--------+-------------+-------------+-------------
 f(α,β) |    β="a"    |    β="b"    |    β="c"    
--------+-------------+-------------+-------------
  α="a" |      0      |      x      |     a-x     
--------+-------------+-------------+-------------
  α="b" |   b-c'+a-x  |      0      |    c'-a+x   
--------+-------------+-------------+-------------
  α="c" | a'-b+c'-a+x |c-a'+b-c'+a-x|      0      
--------+-------------+-------------+-------------

$A[i]=\mathrm{a}$ 有解的条件是存在一个整数 $x$,使得表中每个数均非负,即

$$x\ge\max\{0,a-c',a-c'+b-a'\}$$ $$x\le\min\{a,a-c'+b,a-c'+b-a'+c\}$$

因此当 $\max\{0,a-c',a-c'+b-a'\}\le\min\{a,a-c'+b,a-c'+b-a'+c\}$ 时,说明确定 $A[i]=\mathrm{a}$ 可行。反之,$A[i]=\mathrm{a}$ 不可行,就继续枚举 $A[i]=\mathrm{b}$ 和 $A[i]=\mathrm{c}$ 判断是否可行。

这样就可以确定有解时最小的 $A[i]$。依次对于 $i=1,2,\cdots,n$ 确定最小的 $A[i]$,最后的串 $A$ 就是答案。

时间复杂度 $O(n)$。


Sirni

关键词:最小生成树,优化建图

看完题目,我们首先可以想到一种做法,即把整个完全图建出来,然后用Prim或Kruskal算法求其最小生成树。这样做的复杂度为 $O(n^2)$(使用Prim)或 $O(n^2\log n)$(使用Kruskal),可以得到30分。

这个做法的瓶颈在于边数过多,为 $O(n^2)$ 级别,难以承受 $n=10^5$ 的数据。不过我们其实可以注意到,有很多边是没有用的。

首先有一个明显的性质:点权相同的点只要留一个。因为,如果 $p_i=p_j$,那么点 $i,j$ 间连边不需要代价,显然可以直接连起来,并且把和 $p_j$ 关联的边 $(p_j,v)$ 改成 $(p_i,v)$,代价不变;因此可以直接删掉 $p_j$。

现在的 $p_i$ 两两不同。考虑边 $(u,v)$ 的权值,其中 $u < v$,则权值 $w=\min\{u,v\bmod u\}=v\bmod u$。我们观察当 $u$ 一定,$v$ 增大时权值 $w$ 的变化,发现 $w$ 被分成了若干段 $[u+1,2u),[2u,3u),[3u,4u)\cdots$,每段的 $w$ 是一个公差为1的等差数列:

v u+1 u+2 ... 2u-1 2u 2u+1 2u+2 ... 3u-1 3u 3u+1 3u+2 ...
w  1   2  ...  u-1  0   1    2  ...  u-1  0   1    2  ...

对于每一段 $[ku+[k=1],(k+1)u)$($k$ 为正整数),可能有多个点的权值落在该区间内,我们可以只保留 $u$ 连到该区间内权值最小的点的边

为什么呢?

设该区间内有两个点,权值为 $p,q$,且 $p < q$,则边 $(u,p)$ 的权为 $p-ku$,$(u,q)$ 的权为 $q-ku$,$(p,q)$ 的权为 $q\bmod p$。因为 $q > p$,所以 $q\bmod p\le q-p$。这说明

$$q-ku=(p-ku)+(q-p)\ge(p-ku)+(q\bmod p)$$

也就是说,连边 $(u,q)$ 不如连边 $(u,p),(p,q)$,虽然后者连了两条边,但代价不可能比前者大!

这就证明了往该段中不是最小的点连边不优。去掉这种不优的边以后,剩下的边有多少呢?对于点 $i$,满足 $v > p_i$ 的边 $(p_i,v)$ 最多有 $\min\{{10^7\over p_i},n\}$ 条,所以当 $n=10^5$ 时,边数 $m$ 满足

$$m\le\sum_{i=1}^n\min\{{10^7\over i},n\} < 8\times 10^7$$

现在的边数可以接受了,我们也就能得到大致算法:

  • 去掉相同的 $p_i$;
  • 对每个点 $i$ 和每个正整数 $k$,如果存在点 $j$ 满足 $kp_i+[k=1]\le p_j < (k+1)p_i$,那么取 $p_j$ 最小的一个,连边 $(p_i,p_j)$;
  • 用Kruskal算法求图的最小生成树。

当然程序还需要精细地实现,否则仍然会超时。实现中需要优化的是2、3两个步骤。

  • 第2步的连边复杂度可以优化到 $O(n+m)$($m$ 为边数),方法是预处理大小为 $10^7$ 的数组 $f[x]$ 表示大于等于 $x$ 的最小点权对应的点,这样就可以 $O(1)$ 找出下一个 $k$:
for(int i = 1; i <= n; i++) f[p[i]] = i;
for(int i = 1e7; i; i--) if(!f[i]) f[i] = f[i + 1];
  • 第3步的Kruskal,如果一开始用 $O(m\log m)$ 时间给边排序,容易超时;不过注意到边权只有 $10^7$ 种,不妨开 $10^7$ 个边表(编号 $0$ 到 $10^7-1$)存每种权的边,加边时按权值加入对应边表,最后按顺序遍历边表就是排好序的了。

这样就可以在5s时限内出解,得到100分了。


Poklon

关键词:问题转化,树状数组

下面记给定的数列为 $a_1,a_2,\cdots,a_n$。

不难设计出这样的暴力算法:对于每次询问 $l,r$,用 $O(n)$ 的时间统计 $a_l,a_{l+1},\cdots,a_{r-1},a_r$ 中出现两次的数的个数。怎样统计呢?开一个数组统计每个数出现的次数。然而数值范围是 $[0,10^9]$,这么大的数组开不下,怎么办?

将 $a$ 从小到大排序,得到下标序列 $b$,则 $a_{b_1}\le a_{b_2}\le\cdots\le a_{b_n}$。然后将 $a$ 中最小的数改成 $1$,第二小的数改成 $2$,第三小的数改成 $3$……例如序列 $a=[2,7,1,8,2,8]$ 修改的过程如下:

         i    1 2 3 4 5 6
        b[i]  3 1 5 2 4 6
修改前 a[b[i]] 1 2 2 7 8 8
修改后 a[b[i]] 1 2 2 3 4 4

修改前   a[i]  2 7 1 8 2 8
修改后   a[i]  2 3 1 4 2 4

显然相同的数改完以后仍然相同,不同的数改完以后仍然不同,所以不影响答案。并且现在的数值范围是 $[1,n]$,询问时,可以直接开大小为 $n$ 的数组统计每个数的出现次数了。对于每个询问 $l,r$,实现如下:

  • 开一个大小为 $n$,初值为 $0$ 的数组 $t$,初始答案 $s=0$;
  • 依次对于 $i=l,l+1,\cdots,r-1,r$,将 $t[a_i]$ 增加 $1$:
    • 如果增加后 $t[a_i]=2$,则令 $s$ 增加 $1$;
    • 如果增加后 $t[a_i]=3$,则令 $s$ 减少 $1$;
    • 其余情况 $s$ 不变。
  • 最后 $s$ 的值就是 $t$ 中 $2$ 的个数,即答案。

直接实现的复杂度为 $O(Qn)$,可以获得40分。

要得到更高的分数,则需要进一步观察该过程。假设我们一边做这个过程时一边在序列上做标记,把 $t[a_i]$ 的值标记在 $i$ 位置上,那么该询问的答案就是区间内2标记的个数减去3标记的个数

形式化地,定义长度为 $n$ 的序列 $c$,当 $i$ 位置为2标记时,$c_i=1$,当 $i$ 位置为3标记时,$c_i=-1$,否则 $c_i=0$,那么该询问的答案为

$$\sum_{i=1}^rc_i$$

但问题还没有解决!不同的询问 $l$ 可以不同,即序列 $c$ 可能不同。考虑这样一件事情:一开始序列 $a$ 为空,每次在 $a$ 的左端加入一个数,维护此时的序列 $c$。下面是 $a=[3,2,2,3,2,3,2]$ 的例子:

a:                   2
c: 0  0  0  0  0  0  0

a:                3  2
c: 0  0  0  0  0  0  0

a:             2  3  2
c: 0  0  0  0  0  0 [1]

a:          3  2  3  2
c: 0  0  0  0  0 [1] 1

a:       2  3  2  3  2
c: 0  0  0  0 [1] 1[-1]

a:    2  2  3  2  3  2
c: 0  0 [1] 0[-1] 1 [0]

a: 3  2  2  3  2  3  2
c: 0  0  1 [1]-1[-1] 0

每次加入一个数,$c$ 序列的修改已用 [] 标识,可以发现加入 $a_i$ 时,$c$ 至多有三项发生改变:$a_i$ 的第 $2$ 次出现处 $c$ 由 $0$ 变 $1$(如果有),$a_i$ 的第 $3$ 次出现处 $c$ 由 $1$ 变 $-1$(如果有),$a_i$ 的第 $4$ 次出现处 $c$ 由 $-1$ 变 $0$(如果有)。这里的第几次出现指的都是以 $i$ 为左端点。

算法过程如下:

  • 将询问按照 $l$ 分类(可以直接按 $l$ 排序,也可以开 $n$ 个链表存每种 $l$ 的询问);
  • 假设一开始序列 $a$ 为空,依次对于 $l=n,n-1,\cdots,2,1$:
    • 在序列 $a$ 中加入 $a_i$,并更新序列 $c$ 位于 $a_i$ 的第 $2,3,4$ 次出现处的值(如果有);
    • 处理以 $l$ 为左端点的所有询问:询问 $(l,r)$ 的答案为 $\sum_{i=1}^rc_i$。

查询 $a_i$ 的第 $2,3,4$ 次出现处的值,实际上只要处理出 $a_i$ 的下一次出现位置 $\mathrm{next}[i]$ 即可解决。至于 $\mathrm{next}[i]$ 数组如何处理,同样可以将 $a$ 从小到大排序,相同的 $a_i$ 按下标 $i$ 从小到大排序,得到下标序列 $b$,那么当 $a_{b_i}=a_{b_{i+1}}$ 时,$\mathrm{next}[b_i]=b_{i+1}$,否则 $\mathrm{next}[b_i]$ 不存在。

最后,查询 $\sum_{i=1}^rc_i$ 的复杂度仍然是 $O(n)$ 的,因此我们要用树状数组维护 $c$ 将复杂度优化到 $O(\log n)$:

int T[MaxN]; // 初始值为 0 的树状数组
void add(int i, int x) { // 将 c[i] 增加 x
    for(; i <= n; i += i&-i) T[i] += x;
}
int sum(int i) { // 查询 c[1]+c[2]+...+c[i]
    int s = 0;
    for(; i; i -= i&-i) s += T[i];
    return s;
}

那么加入 $a_i$ 时,更新 $c$ 的过程如下(假设不存在的 next[i] 值为 n+1,且 next[n+1]=n+1):

add(next[i],1);
add(next[next[i]],-2);
add(next[next[next[i]]],1);

询问 $l,r$ 的答案,用 sum(r) 即可。

最后这个问题就完美解决了,时间复杂度 $O((n+Q)\log n)$,可以获得100分。

正睿NOIP普及组模拟赛第3场题解

2017-09-24 20:14:32 By oi_loser

由于出题人并不会评估普及组题目难度,这次的题目还是偏难了……

下面是题解。


祖玛

关键字:模拟

首先可以用循环语句,从字符串的第 $x$ 个字符开始往左找第一个不是 $c$ 的字符,假设它是第 $l$ 个字符(如果找不到则 $l=0$),然后从第 $x+1$ 个字符开始往右找第一个不是 $c$ 的字符,假设它是第 $r$ 个字符(如果找不到则 $r=n+1$)。

如果 $r-l < 3$,说明把珠子射入以后,射入的珠子所在的连续段不足 $3$ 个珠子,这时候,应该先输出前 $x$ 个字符,再输出字符 $c$,最后输出第 $x+1$ 到第 $n$ 个字符。

如果 $r-l\ge 3$,说明射入之后 $l+1$ 到 $r-1$ 这一段会消失。如果 $l,r$ 两个位置是不同字符,过程结束;如果是相同字符,设其为 $c'$,从 $l$ 往左找第一个不是 $c'$ 的位置 $l'$,从 $r$ 往右找第一个不是 $c'$ 的位置 $r'$,如果 $(l-l')+(r'-r)\ge 3$,那么 $l'+1$ 到 $l$、$r$ 到 $r'-1$ 会消除,否则不会消除;如果会消除,令 $l=l'$,$r=r'$ 重复此过程即可。可以用 while 循环来实现。最后应该先输出前 $l$ 个字符,再输出第 $r$ 到第 $n$ 个字符。


数列分割

关键字:枚举,减少枚举量

显然可以用两重循环枚举两个切割点 $p,q$ 的位置,再用一重循环计算三个段的最大值,求出最大和最小的答案。但是这样运算量是 $n^3$ 级别的,太慢了。

当然可以少枚举一些东西。比如我们可以开两个数组 L[i]R[i] 分别存储前 $i$ 个数的最大值和第 $i$ 到第 $n$ 个数的最大值。

for(int i = 1; i <= n; i++) L[i] = L[i-1] > a[i] ? L[i-1] : a[i];
for(int i = n; i >= 1; i--) R[i] = R[i+1] > a[i] ? R[i+1] : a[i];

还可以一边枚举 $q$ 一边更新中间段的最大值:

for(int p = 1; p < n; p++){
    int m = 0;
    for(int q = p; q < n; q++){
        if(a[q] > m) m = a[q];
        // m 即为 [p,q] 段的最大值
    }
}

这样左段、中段、右段的最大值不需要额外计算了。这样的运算量还是 $n^2$ 级别的(只要枚举 $p,q$),尽管能得到很多分,但由于超时的缘故还是不能得到满分。

先考虑三段和的最大值怎么求,它就是数列中前三大的数的和

  • 比如数列 $3,5,1,4,3$ 前三大的数是 $5,4,3$,那么不管怎么切,三段加起来不会超过 $5+4+3$。让三段加起来等于 $5+4+3$ 只要一刀切在第2项和第4项之间,一刀切在第4项和第5项之间就好了,$[3,5],[1,4],[3]$ 之类的都行,答案就是 $5+4+3=12$。

再考虑三段和的最小值怎么求,我们只要枚举中段只有1个数或者左右两段都只有1个数的分割方法。因为如果整个数列的最大的数是 $a_k$(如果有几个数都是最大的,随便找一个),那么 $a_k$ 被分到的段肯定越长越好:

  • 若 $a_k$ 在左段或右段,可以让中段只有1个数。比如[3,5],[4,1],[3]显然把左段加长更好:[3,5,4],[1],[3]。这种用一个 for 循环枚举中段包含哪一个数,用刚才处理出的 L[]R[] 数组求答案。
  • 若 $a_k$ 在中段,可以让左右两段都只有1个数。比如[3],[5,1],[4,3]显然把中段加长更好:[3],[5,1,4],[3]。这种直接按 $1,n-2,1$ 分割然后算答案就好了。

在这些情况中求最小的三段和即可,这样枚举量只有 $n$ 级别了,可以得到100分。


秋游

关键字:贪心,问题转化

如果 $m=0$,只要考虑学生的满意度。对每个 $k$,要让 $k$ 个学生秋游,一定是让 $a_i$ 最大的那 $k$ 个学生去。于是把 $a_i$ 排序以后,从大到小一边累加一边输出。(下面这个代码要 #include <algorithm>

std::sort(a + 1, a + n + 1);
int s = 0; // 总满意度
for(int i = n; i; i--) printf("%d\n", s += a[i]);

如果 $m>0$,需要考虑朋友关系,看起来不是很好处理。

可以用搜索算法(如深度优先搜索)搜索所有可能的情况,对每种情况计算满意度。然而 $n$ 个学生一共有 $2^n$ 种选取方式,对于 $n=100000$ 的数据搜索是一定会超时的。我们需要一些更好的处理方法。

对于一对朋友关系 $j$,它对满意度的影响可以表示成 $(t-1)\cdot w_j = t\cdot w_j - w_j$,其中 $t$ 表示 $u_j,v_j$ 两人中,有几个人去了秋游。因此在计算总满意度的时候,可以先把总满意度减掉 $w_j$,然后 $u_j$ 和 $v_j$ 每有一个人去秋游,总满意度加 $w_j$。

所以只要对于每对朋友关系 $j$,把 $a_{u_j}$ 和 $a_{v_j}$ 加上 $w_j$,最后答案减掉 $w_j$,就可以不管这条边了。这样就把问题转化成了 $m=0$ 的情况。排序以后输出 $a$ 累加的结果减去所有 $w_j$ 的和就行了:

int s = 0; // 总满意度
for(int j = 1; j <= m; j++) a[u[j]] += w[j], a[v[j]] += w[j], s -= w[j];
std::sort(a + 1, a + n + 1);
for(int i = n; i; i--) printf("%d\n", s += a[i]);

这样就可以轻松得到满分了。


兔子繁殖

关键字:递推,前缀和

1. 80分算法

稍微有点常识就会发现,兔子的繁殖数量是指数级增长的,用简单的模拟法,记录每对兔子的年龄,计算机内存无法接受。

注意到所有兔子的年龄都在 $0$ 到 $n-1$ 之间,不妨换一种模拟方法:记录每个月,年龄为 $0$ 的兔子有多少对,年龄为 $1$ 的兔子有多少对,……,年龄为 $n-1$ 的兔子有多少对。开一个数组 f[i] 表示年龄为 $i$ 的兔子数量,那么每个月所有兔子的年龄增加 $1$ 就是这个操作:

for(int i = n - 1; i >= 0; i--) f[i + 1] = f[i];
f[0] = 0;

繁殖则是这样的操作:

for(int i = 0; i < m && k + i * t < n; i++) f[0] += f[k + i * t];

这样你会发现一个问题:使用32位整数 int 类型甚至64位整数 long long 类型都会溢出!这时候仔细看题目,题目只要输出答案除以 $10007$ 的余数,也就是如果答案为 $x$,输出的是 $x\bmod 10007$。并且我们知道模运算有很好的性质,那就是对任意整数 $a,b$ 和正整数 $p$,有:

$$(a+b)\bmod p=a\bmod p+b\bmod p$$

所以任何时候我们都只要记录当前的兔子数量除以10007的余数。只要把繁殖操作改一改:

const int mod = 10007;
for(int i = 0; i < m && k + i * t < n; i++) f[0] = (f[0] + f[k + i * t]) % mod;

这样就能求出第 $m$ 个月每种年龄的兔子有多少对,把未死亡的兔子数量加起来以后求出除以10007的余数输出就是答案了。

很好,这样的运算量是 $n^2$ 级别的,可以轻松通过 $n\le 2000$ 的测试数据了。当然我们注意到这题有一类特殊的测试数据:$k\le 4,t=1,m=n$。这意味着什么?每对兔子出生至多 $4$ 个月以后,每个月都会生出兔子,且 $n$ 个月以内不会死亡。那么年龄在 $4$ 以上的兔子都是一样的,只要记录年龄为 $0,1,2,3,4$ 的兔子数量这五个信息就行了,只要 $5n$ 级别的运算量。

把 $n\le 2000$ 和 $k\le 4,t=1,m=n$ 的两个程序用 if 语句拼起来,可以获得80分。

2. 100分算法

遗憾的是上面的算法并不能通过一般的 $n=10^7$ 的测试数据,毕竟用 for 循环从 $1$ 遍历到 $10^7$ 都需要不少的时间了。

其实我们只要考虑刚出生的兔子就行了。因为兔子的出生时间确定了,繁殖和死亡时间也就确定了。第 $n$ 个月未死亡的兔子就是出生在第 $n-k-(m-1)t+1$ 个月到第 $n$ 个月的兔子数量。

开一个数组 $f[i]$ 存第 $i$ 个月出生的兔子有多少对(注意还是只要存除以10007的余数),显然 $f[1] = 1$。当 $i>1$ 时怎么计算 $f[i]$ 呢?

对于第 $i$ 个月,可以发现,第 $i-k,i-k-t,i-k-2t,\cdots,i-k-(m-1)t$ 这些月份出生的每对兔子恰好生出一对第 $i$ 个月出生的兔子,而其余月份出生的兔子不会生成第 $i$ 个月出生的兔子。因此可以得到递推式

$$f[i]=f[i-k]+f[i-k-t]+f[i-k-2t]+\cdots+f[i-k-(m-1)t]$$

如果下标是负的就当成 $0$。如果直接用这个式子递推,求和需要花费比较多的时间。不过等号右边的 $f$ 的下标除以 $t$ 的余数都相同。比如 $t=3$,如果我们把 $f$ 数组摆成这样:

f[1]        f[4]        f[7]        f[10]            ...
    f[2]        f[5]        f[8]        f[11]        ...
        f[3]        f[6]        f[9]        f[12]    ...

那么对某个 $f[i]$,它的值是上面这个数组某一行的某一个连续段的和

这可以用前缀和技巧来加速计算,也就是我们开一个数组 $g[i]$ 存上面的 $f[i]$ 往左的所有数之和。

例如当 $t=3$ 时,$g[11]=f[11]+f[8]+f[5]+f[2]$。这样用前缀和相减就可以快速得到一段区间的和。还是假设 $t=3$,比如要算 $f[5]+f[8]+f[11]$,只需两端的 $g$ 相减,即 $g[11]-g[2]$。

处理这个 $g$ 数组非常简单,如果 $i\le t$,那么 $g[i]=f[i]$,否则 $g[i]=f[i]+g[i-t]$ 这样递推就好了。

于是按 $i$ 从小到大依次推出 $f[i],g[i]$,最后统计第 $n-k-(m-1)t+1$ 个月到第 $n$ 个月的兔子有多少对就做完了,注意负下标要当成 $0$。可以获得100分。

提高#2题解还原版

2017-09-03 21:50:15 By oi_loser

以下题解由WuHongxun(出题人)发布的视频题解进行文本还原。


A:Last mile of the way

算法一

对每个询问暴力背包DP即可。期望得分10分。

算法二

用 $f[x][s]$ 表示以 $x$ 为根的子树中取一些大小和不超过 $s$ 的节点,最大权值和为多少。转移考虑逐个将 $x$ 的子节点合并到 $x$ 子树中,合并子节点 $c$ 时:

$$f[x][s] \leftarrow f[c][s'] + f[x][s - s']$$

复杂度 $O(ns^2)$。期望得分30分。

算法三

树是一条链,每个询问相当于询问链的一个后缀的背包。

$f[x][s]$ 表示后 $x$ 个结点取大小和不超过 $s$ 的节点,最大权值和是多少。

每次加入一个物品:

$$f[x][s] \leftarrow f[x][s – size[x]] + weight[x]$$

结合算法二期望得分50分。

算法四

之前做法的瓶颈在于合并子树的复杂度较高。可以用启发式合并的trick,即每次合并时暴力DFS物品较少的子树。

比如一个点有多个孩子,其中一个孩子物品最多,则合并时把最大的这个孩子的DP数组直接拷贝到父亲上,对于其它孩子暴力DFS,把这些物品一个个加进来。

显然每次加入物品是 $O(s)$ 的。每加一次物品,由于它不在最大的子树里,它所在的集合大小至少会乘2,由于总的集合大小为 $n$,所以每个物品最多被加 $O(\log n)$ 次。复杂度 $O(ns\log n)$。期望得分100分。


B:Reason For Living

算法一

最大化生成森林的边数,等价于选一个极大的生成森林,即无法加入一条边使得生成森林更大。

暴力用并查集维护,由于每次至少会删去一条边,这样复杂度是 $O(m^2)$。

期望得分40分。如果不会并查集,用其它方法可能可以得到20分。

算法二

考虑一条边一条边往图中加,维护若干个生成森林,显然每条边应加到第一个两端点不连通(或者说加入后生成森林里没有环)的森林中。

每个生成森林都用一个并查集维护,二分出第一个两端点不连通的并查集。显然使它们连通的并查集是一个前缀,因为我们每次取的是极大的生成森林。

现在的问题是,如果用 $m$ 个并查集,如果每一个生成森林都用一个并查集维护,是不是需要 $nm$ 个点?其实可以非常简单地处理,对于一个度数为 $d$ 的点,它最多只会在前 $d$ 个生成森林里出现,把这个点放到前 $d$ 个生成森林里。由于每条边最多给两个点的度数加1,所以所有并查集只需要 $2m$ 个点。

复杂度 $O(m\log m\cdot\alpha(n))$。期望得分80分。

算法三

见链接:http://oj.acyume.com/blogof/wuhongxun/blog/20

我也没懂,不过直接写80分算法可以100分:http://oj.acyume.com/submission/2410


C:World Of Our Own

算法一

按照题意暴力 $O(n^2)$ 模拟即可。期望得分30分。

算法二

$d\le 50$,观察数据生成方式,$A$ 中每一项只和上一项有关,由于前 $d+1$ 个数中必有两数相同,也就是 $A$ 会出现循环节然后一直循环下去,循环节第一次出现位置和长度均不超过 $d$。

利用循环节的性质,只对第一次出现循环节之前的部分和循环节进行模拟即可,复杂度 $O(nd)$。结合算法一期望得分70分。

算法三

这个过程可以看成在每次在 $A$ 的上方每个位置写上它下方和右下方的数的异或,并且每次长度减1。它等价于 $A$ 中每个点每次往上走一步或往左上走一步,有多少种方案走到这个点。假设 $A$ 中的点为 $i$(初始数列的第 $i+1$ 项),另一个点为 $j$(第 $j$ 个数列的第一项),那么 $i$ 点到 $j$ 点会往上走 $j$ 步,并且正好往左上走了 $i$ 步,也就是走上去的方案数正好是 $C_j^i$。

也就是第 $j$ 个数列的第一项实际上是 $C_j^0$ 个 $a_1$,$C_j^1$ 个 $a_2$,…,$C_j^i$ 个 $a_{i+1}$,…,$C_j^j$ 个 $a_{j+1}$ 异或起来。如果 $i$ 到 $j$ 有奇数种方案,那么 $i$ 到 $j$ 就有贡献,否则就没有贡献。也就是要考虑 $C_j^i$ 的奇偶性。

根据Lucas定理:

$$C_j^i\bmod 2=C_{\lfloor\frac{j}{2}\rfloor}^{\lfloor\frac{i}{2}\rfloor}\cdot C_{j\bmod 2}^{i\bmod 2}\bmod 2$$

实际上相当于每次把 $i,j$ 的二进制表示的最后一位 $i\bmod 2,j\bmod 2$ 取出来,然后删掉。$C_{j\bmod 2}^{i\bmod 2}=0$ 当且仅当 $j\bmod 2=0$ 而 $i\bmod 2=1$。那么 $C_j^i$ 为奇数当且仅当每个二进制位都没有这种情况,也就是 $i$ 的二进制中1的位是 $j$ 的二进制中1的位的一个子集,即

$$i\ \mathrm{and}\ j=i$$

对于每个 $j$ 的答案,记为 $b_j$,它等于所有满足 $i\ \mathrm{and}\ j=i$ 的 $a_i$ 的异或和。怎么做呢?

注意到这里的子集可以看成一个高维前缀和,可以看成数组 $a$ 有 $m$ 维,每一维都是 $0$ 或 $1$,表示下标的一个二进制位。我们要对 $a$ 求一个 $m$ 维的前缀和得到 $b$,$b_j$ 就是把 $j$ 中的一些 $1$ 的位变成 $0$ 得到 $i$ 的 $a_i$ 的和。

我们知道,二维前缀和有两种方法,第一种方法是这样:

for(int i = 1; i <= n; i++)
    for(int j = 1; j <= n; j++)
        s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];

实际上还有第二种方法,每次把一维做前缀和:

for(int i = 1; i <= n; i++)
    for(int j = 1; j <= n; j++)
        a[i][j] += a[i - 1][j];
for(int i = 1; i <= n; i++)
    for(int j = 1; j <= n; j++)
        a[i][j] += a[i][j - 1];

现在我们同样用第二种方法。因为 $n\le 8\times 10^6$,下标的二进制只有 $23$ 位,所以可以令 $m=23$,依次枚举 $0,1,\cdots,22$ 每一维做前缀和即可:

for(int j = 0; j < 23; j++)
    for(int i = 0; i <= n; i++)
        if (i >> j & 1) a[i] ^= a[i ^ (1 << j)];

复杂度 $O(n\log n)$。期望得分100分。事实上这题可以用压位的trick进一步降低复杂度,不过这题直接这样实现就可以过了。

oi_loser Avatar