POI - B
POI - B
A. [POI 2005] Cash Dispenser (bank) (思维题)
题意
有一个 \(4\) 位 PIN 码,现在记录下用户输入 PIN 码时手指移动位置的序列,序列中每一位数可能不按,也可能按若干次。
对于同一个 PIN 码,给定记录下的 \(n\) 个序列(序列 \(i\) 长度为 \(t_i\)),求可能的序列个数。
\(1\le n\le1000\),\(1\le t_i\le10000\),\(\sum t_i\le10^6\)。
思路
首先解释一下题意:设一个数字密码串的「位置序列」为相邻相同数字去重后的序列,如 \(22333\) 的位置序列为 \(23\)。给定 \(n\) 个数字串,求可能的 \(4\) 位数字密码个数,使得密码的位置序列是这 \(n\) 个数字串的子序列(可能不连续)。如,给定 \(123\) 和 \(234\),那么密码的位置序列是公共子序列 \(23\) 的子序列,所以可能的密码有 \(5\) 种:\(2222,2223,2233,2333,3333\)。
题目要求我们求满足「位置序列 是 所有 \(n\) 个数字串的子序列」的串的个数,那么我们不妨转化一下:
因为所求的串一定是四位数,一共只有 \(10^4\) 个,所以我们可以考虑标记出「对于某个 给出数字串,位置序列 不是 其子序列」的串,最后数出没有被标记的串的个数即可。
对于读入的每一个数字串,我们需要标记所有不合法的数字。枚举 \([0,10^4)\) 内的所有数字,判断其是否是所读入的数字串的子序列。
如何快速判断?记 \(p(i,j)\) 为第 \(i\) 位后面第一个 \(j\) 的位置,如果没有则设为 \(-1\)。我们选择用先进先出的队列处理。具体实现参考代码。
于是,由于只有四位数,我们可以在常数时间内判断一个数字是否合法。如果不合法则在值域数组中打上标记。
在处理完所有数字串后,我们枚举 \([0,10^4)\) 内的所有数字,记录没有被标记过的数的个数即为答案。
时间复杂度 \(O\left(w\sum t_i+nm\right)\),其中 \(w=10\),\(m=10^4\)。
代码
#include <iostream>
#include <string>
#include <queue>
#define g(x, y, z) for (int x = (y); x < (z); ++x)
using namespace std;
const int N = 5e5 + 10;
int n, t, p[N][10];
bool ok[10001];
string s;
queue<int> q[10];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
while (n--) {
cin >> t >> s;
int len = s.length();
g(i, 0, len) q[s[i] - '0'].push(i);
g(i, 0, len) {
g(k, 0, 10) {
if (q[k].empty()) p[i][k] = -1;
else p[i][k] = q[k].front();
}
q[s[i] - '0'].pop();
}
g(x, 0, 1e4) {
if (!ok[x]) {
int i = x/1000, j = x/100-i*10, k = x/10-i*100-j*10, l = x%10;
if (p[0][i] == -1 || p[p[0][i]][j] == -1 || p[p[p[0][i]][j]][k] == -1 || p[p[p[p[0][i]][j]][k]][l] == -1)
ok[x] = true;
}
}
}
int ans = 0;
g(x, 0, 1e4) ans += (!ok[x]);
cout << ans << '\n';
return 0;
}
B. [POI 2005] Toy Cars (sam) (贪心)
题意
两个正整数集 \(A\) 和 \(B\),初始时 \(A\) 中有 \(1\) 到 \(n\) 共 \(n\) 个数,\(B\) 为空。数字只能在 \(A\) 和 \(B\) 之间来回移动。
现在有长度为 \(p\) 的序列 \(a\),\(a_i\) 表示在时刻 \(i\) 需要满足 \(a_i\) 在 \(B\) 中。且在同一时刻 \(B\) 中最多只能有 \(k\) 个数。
如果 \(B\) 中已经有 \(k\) 个数,则必须先选择 \(B\) 中的一个数移动到 \(A\) 中,才可以将 \(A\) 中一个数移动到 \(B\) 中。
求最少需要将数字从 \(A\) 到 \(B\) 移动几次。
\(1\le k\le n\le10^5\),\(1\le p\le5\times10^5\),\(1\le a_i\le n\)。
思路
如果迫不得已选择一个数从 \(B\) 移动到 \(A\),一定是选择当前 \(B\) 中下一次需要最晚的数 \(i\)。
感性理解:如果选择一个下一次需要比 \(i\) 早的数 \(j\) 拿走,那么后面把 \(j\) 拿回来之后还需要把 \(i\) 拿回来,这样一定不优。
于是用堆维护 \(B\) 中的数,以下一次需要的时间为关键词从大到小排序。并且记录每个数是否在堆里。每次需要放进新的数时,如果 \(B\) 满了就拿走堆顶。
现在有一个问题:如果需要的数已经在堆中,如何更新它的下一次被需要的时间?
观察到一个数被需要的时间一定是单调递增的,也就是说,只要堆中加入了新的时间,那么原来的时间必不可能作为堆顶,也就完全没用了。
为了避免将原来的时间拿出来,我们可以把原来的时间就留在堆中,同时将 \(k\) 加一。(妙啊!
答案直接记录即可。复杂度 \(O(p\log p)\)。
代码
#include <iostream>
#include <queue>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 10, P = 5e5 + 10;
const int INF = 0x3f3f3f3f;
int n, k, p, lst[N], nxt[P], a[P], ans;
priority_queue<pii> q;
bool in[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k >> p;
g(i, p, 1) {
cin >> a[i];
if (!lst[a[i]]) nxt[i] = INF; //永远不再需要
else nxt[i] = lst[a[i]];
lst[a[i]] = i;
}
f(i, 1, p) {
if (!in[a[i]]) {
if (q.size() == k) {
in[q.top().second] = false;
q.pop();
}
in[a[i]] = true;
++ans;
} else ++k; //重点!!!!
q.emplace(nxt[i], a[i]);
}
cout << ans << '\n';
return 0;
}
C. [POI 2005] Piggy Banks (ska) (并查集)
题意
有 \(n\) 个存钱罐,存钱罐中不仅有钱,还有若干存钱罐的钥匙,第 \(i\)(\(1\le i\le n\))个存钱罐的钥匙在第 \(x_i\) 个存钱罐中。
求最少需要暴力砸开几个存钱罐,使得所有存钱罐都能被打开。
\(1\le n\le10^6\),\(1\le x_i\le n\)。
思路
如果一个存钱罐被打开(不管是暴力砸开还是用钥匙),那么可能会引起一系列存钱罐都被打开。
于是我们用并查集维护,如果把根节点对应的存钱罐打开,那么同一个并查集中的存钱罐都可以被打开。
输入时直接合并,把 \(x_i\) 作为 \(i\) 的根。由于 \(\boldsymbol{x_i}\) 和 \(\boldsymbol{i}\) 是一一对应的,所以并不存在指向非根节点的情况,因此每次都是直接把一棵树形结构合并到另一棵上。
最后枚举所有节点,数出一共几个并查集即可。
复杂度 \(O(n\alpha(n))\)。
代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
const int N = 1e6 + 10;
int n, ans;
bool vis[N];
int fa[N];
int getfa(int x) { return x == fa[x] ? x : fa[x] = getfa(fa[x]); }
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
f(i, 1, n) fa[i] = i;
f(i, 1, n) {
int j;
cin >> j;
int fi = getfa(i), fj = getfa(j);
if (fi != fj) fa[fi] = fj;
}
f(i, 1, n) {
int fi = getfa(i);
if (!vis[fi]) vis[fi] = true, ++ans;
}
cout << ans << '\n';
return 0;
}
D. [POI 2005] A Journey to Mars (lot) (单调队列)
题意
有 \(n\) 个空间站呈环形排列,从 \(1\) 到 \(n\) 编号。
有一个人在其中一个空间站登陆,他的目标是顺时针或逆时针绕一圈回到初始位置。
走 \(1\) 米需要消耗 \(1\) 升油。油箱没有容量限制。
第 \(i\) 号空间站的信息有两个:
- \(p_i\) 表示该空间站可以补给的油量,单位为升;
- \(d_i\) 表示 \(i\) 到 \(i+1\) 的距离(\(d_n\) 表示 \(n\) 到 \(1\) 的距离),单位为米。
对于每个空间站,请你判断在此登陆是否可以绕一圈(顺时针或逆时针)回到登陆位置。
\(3\le n\le10^6\),\(p_i\ge0\),\(d_i>0\),\(\sum d_i\le2\times10^9\)。
思路
首先破环为链。顺时针和逆时针的区别只有向左还是向右,这里先说向右。
我们发现,如果一个位置 \(i\)(\(1\le i\le n\))不合法,那么一定是有一个位置 \(j\in[i,i+n)\),满足
从而使 \(j\) 到不了 \(j+1\)。也就是说,如果位置 \(i\) 可行,那么对于所有 \(j\in[i,i+n)\),必须满足
于是设前缀和 \(s_i=\sum\limits_{j=1}^ip_j-d_j\),则上式变为 \(s_j-s_{i-1}\ge0\;(\forall\,j\in[i,i+n-1])\),即
于是问题就变成了单调队列维护固定区间最小值的问题。由于区间在所遍历的 \(i\) 的右侧,所以对于这种情况要 从右到左 遍历。
向左同理,\(\forall\,j\in[i-n+1,i]\) 满足 $ p_i+p_{i-1}+\dots+p_j\ge d_{i-1}+d_{i-2}+\dots+d_{j-1}$,
即 \(\sum\limits_{k=j}^i\,p_k-d_{k-1}\ge0\)。设 \(s_i=\sum\limits_{j=1}^ip_j-d_{j-1}\)。则上式变为
为了方便,我们将 \(i\) 替换为 \(i+1\),将 \(j\) 替换为 \(j+1\),上式变为
其中 \(0\le i\le2n-1\)。于是就可以愉快地用单调队列维护了。别忘了是 从左到右 遍历。
答案可以用布尔数组来存,只要有一个方向合法即可,所以是或运算。
时间复杂度 \(O(n)\)。
代码
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
const int N = 1e6 + 10;
int n, p[N], d[N], s[N << 1];
int q[N << 1], h, t;
bool ans[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
f(i, 1, n) cin >> p[i] >> d[i];
f(i, 1, n) s[i] = s[i + n] = p[i] - d[i];
f(i, 1, n << 1) s[i] += s[i - 1];
h = t = 1;
q[h] = n << 1 | 1;
g(i, n << 1, 1) {
while (h <= t && q[h] > i + n - 1) ++h;
if (i < n) ans[i] |= (s[q[h]] - s[i - 1] >= 0);
while (h <= t && s[q[t]] >= s[i - 1]) --t;
q[++t] = i - 1;
}
d[0] = d[n];
f(i, 1, n) s[i] = s[i + n] = p[i] - d[i - 1];
f(i, 1, n << 1) s[i] += s[i - 1];
h = t = 1;
q[h] = 0;
f(i, 0, (n << 1) - 1) {
while (h <= t && q[h] < i - n + 1) ++h;
if (i + 1 > n) ans[i + 1 - n] |= (s[i + 1] - s[q[h]] >= 0);
while (h <= t && s[q[t]] <= s[i + 1]) --t;
q[++t] = i + 1;
}
f(i, 1, n) cout << (ans[i] ? "TAK\n" : "NIE\n");
return 0;
}
E. [POI 2005] Bank Notes (ban) (多重背包优化)
题意
一共有 \(n\) 种面值的硬币,第 \(i\)(\(1\le i\le n\))种硬币的面值为 \(b_i\),数量为 \(c_i\),现在我们想要凑出面值 \(k\),求最少要用多少个硬币。要求输出一种选硬币的方案。
\(1 \le n \le 200\),\(1 \le b_1 < b_2 < \cdots < b_n \le 2 \times 10^4\),\(1 \le c_i \le 2 \times 10^4\),\(1 \le k \le 2 \times 10^4\)。
思路
多重背包模板题,但是输出方案((
我们采用二进制优化。把 \(c_i\) 分成 \(2^0+2^1+2^2+\dots+2^p+m\),其中 \(2^p-1\le c_i\),\(2^{p+1}-1>c_i\),\(0\le m<2^{p+1}\)(也就是令 \(p\) 极大)。
这样分组的目的是能凑出 \([0,c_i]\) 内的所有数字。接下来就是普通的 0/1 背包了,设 \(f(j)\) 表示凑出面值 \(j\) 最少需要多少个硬币。
但是这道毒瘤题还让我们记录转移点,于是新开一个数组 \(r\)(record)记录。
最后回去找方案可以用 DFS。时间复杂度 \(O(k\sum\log c_i)\)。
代码
#include <cstdlib>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
const int N = 210, M = 3e3 + 10, V = 2e4 + 10;
int n, b[N], c, k, cnt, w[M], v[M], f[V], r[M], ans[N];
bool vis[M];
void dfs(int x) {
if (!x) {
f(i, 1, n) cout << ans[i] << ' ';
cout << '\n';
exit(0); //结束程序
}
g(i, cnt, 1) {
if (vis[i] || w[i] > x) continue;
if (f[x] != f[x - w[i]] + v[i]) continue;
vis[i] = true;
ans[r[i]] += v[i];
dfs(x - w[i]);
ans[r[i]] -= v[i];
vis[i] = false;
}
return;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
f(i, 1, n) cin >> b[i];
f(i, 1, n) {
cin >> c;
int p = 1;
while (c - p > 0) {
c -= p;
w[++cnt] = b[i] * p, v[cnt] = p;
r[cnt] = i;
p <<= 1;
}
if (c) w[++cnt] = b[i] * c, v[cnt] = c, r[cnt] = i;
}
cin >> k;
memset(f, 0x3f, sizeof f);
f[0] = 0;
f(i, 1, cnt) g(j, k, w[i])
f[j] = min(f[j], f[j - w[i]] + v[i]);
cout << f[k] << '\n';
dfs(k);
return 0;
}
F. [POI 2005] Fibonacci Sums (sum) (思维题,Zeckendorf 表示法)
题意
设 \(F(i)\) 表示 Fibonacci 数列的第 \(i\) 项,即 \(F(i)=\begin{cases}1, & i=0 \lor i=1, \\ F(i-1)+F(i-2), & i\ge2.\end{cases}\)
一个数 \(c\) 的 Fibonacci 表示是指一个 \(0/1\) 数列 \(b_1,b_2,\dots,b_n\) 满足:
- \(\forall\,i\in[1,n],b_i\in\{0,1\}\);
- \(c=b_1\times F(1)+b_2\times F(2)+\dots+b_n\times F(n)\)(不使用 \(F(0)\) );
- 如果 \(n>1\),那么 \(b_n=1\),即不包含前导零;
- 如果 \(b_i=1\),那么 \(b_{i+1}=0\),即不使用两个(或多个)连续的数字。
给出两个正整数的 Fibonacci 表示(长度为 \(n,m\)),计算其和的 Fibonacci 表示。
\(1\le n,m\le10^6\)。
思路
我们先将两个数列按位相加,再考虑如何转化为合法形式。按位相加之后,会形成一个只含有 \(0,1,2\) 的数列 \(z\)。
我们从高到低考虑每一位 \(i\),且保证已经考虑过的 \(\ge i+1\) 的更高位合法(即只含 \(0,1\),且没有相邻两个 \(1\))。
首先,如果没有 \(2\),那么对于两个连续的 \(1\),直接递推地向上进位即可。由于高位合法,这样做之后数列也是合法的。如下面的 flush 函数:
void flush(int p) { while (z[p] && z[p + 1]) --z[p], --z[p + 1], ++z[p + 2], p += 2; }
对于从高到低遍历的位置 \(i\),我们首先 flush(i)。
考虑含有 \(2\) 的情况。对于一个 \(2\),我们发现 \(0200=0111=1001\)(左边为高位)。于是可以把这个 \(2\) 变为 \(1\) 放到其他位。
具体地,若 \(z_i\ge2\),那么令 \(z_i\gets z_i-2,z_{i+1}\gets z_{i+1}+1,z_{i-2}\gets z_{i-2}+1\)。这种操作我们称作 \(op(i)\)。
做完 \(op(i)\) 之后,发现影响了 \(i+1\) 位,于是直接 flush(i+1),从而更新更高位使其合法。
虽然此时会出现低位为 \(3\) 的情况,但是我们可以不用管它。如果当前 \(z_i=3\),\(op(i)\) 之后加一个 flush(i) 即可。
特别地,\(F(0)=F(1)=1,F(2)=2\),因此 \(op(1)\) 为 \(z_1\gets z_1-2,z_2\gets z_2+1\),\(op(2)\) 为 \(z_2\gets z_2-2,z_1\gets z_1+1,z_3\gets z_3+1\)。
答案要求没有前导零,从 \(\max(n,m)+2\) 开始试最高位,一定是对的。
时间复杂度分析:考虑 \(d=\sum z_i\),每次进位均会让 \(d\) 减少 \(1\),而 \(op\) 不改变 \(d\),故
flush的总复杂度均摊为 \(d\),即总时间复杂度线性。
代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 1e6 + 10;
int m, n, x, z[N];
void flush(int p) {
while (z[p] && z[p + 1]) --z[p], --z[p + 1], ++z[p += 2];
return;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> m;
f(i, 1, m) cin >> x, z[i] += x;
cin >> n;
f(i, 1, n) cin >> x, z[i] += x;
n = max(n, m);
g(i, n, 1) {
flush(i);
if (z[i] >= 2) {
if (i == 1) z[1] -= 2, ++z[2];
else if (i == 2) z[2] -= 2, ++z[1], ++z[3];
else z[i] -= 2, ++z[i - 2], ++z[i + 1];
flush(i + 1), flush(i);
}
}
g(i, n + 2, 1) if (z[i]) {
cout << (n = i) << ' ';
break;
}
f(i, 1, n) cout << z[i] << ' ';
cout << '\n';
return 0;
}
G. [POI 2005] Template (sza) (失配树,双向链表)
题意
你打算在纸上印一串字母。
为了完成这项工作,你决定刻一个印章。印章每使用一次,就会将印章上的所有字母印到纸上。
同一个位置的相同字符可以被印多次。求出印章上的字符串的最小长度。
例如:用 ababbaba 印出 ababbababbabababbabababbababbaba:
ababbababbabababbabababbababbaba
ababbaba
ababbaba
ababbaba
ababbaba
ababbaba
字符串长度 \(|str|\le5\times10^5\)。
思路
下面的「\(i\) 前缀」表示原字符串长度为 \(i\) 的前缀字符串;Fail Tree 上的一个节点 \(u\) 既可以表示 \(u\) 前缀,又可以表示其长度 \(u\)。
观察样例,我们可以发现,印章上的字符串一定是原字符串一个的 border。
进一步观察,发现这个 border 的出现位置间隔不超过其本身的长度。
考虑如何转化为 Fail Tree 上的问题。
设印章上的字符串为 \(s\),其长度为 \(|s|\),一共需要印 \(k\) 次,每次印的范围为 \([l_i,r_i]\),那么 \(s\) 一定是原字符串的 \(r_i\) 前缀的 border。
也就是说,在 Fail Tree 上,\(s\) 一定是 \(r_i\) 的祖先。
由于 \(r_k=n\),因此 \(s\) 一定在 Fail Tree 中 \(0\) 到 \(n\) 的路径上。于是我们缩小了可能的 \(s\) 的范围。
现在 \(s\) 满足了「是原字符串的一个 border」的条件,还需要满足「出现位置间隔不超过其本身的长度」的条件。
对于一个 \(s\) 一定有一个集合,其中的元素 \(r_i\) 表示可以印的区间,即满足 \(s\) 是原字符串的 \(r_i\) 前缀的一个 border。
这个集合放在 Fail Tree 上,也就是以 \(s\) 为根的子树。
我们考虑在 Fail Tree 上从 \(0\) 到 \(n\) 遍历所有可能的 \(s\)。可以发现以 \(s\) 为根的子树是越来越小的。
对于一个 \(s\),我们将集合 \(\{r_i\}\) 排序,那么只要满足对于任意两个 \(r_i\) 和 \(r_{i+1}\),它们的差不超过 \(|s|\) 即可。
在 Fail Tree 中向下遍历时,我们需要删除集合中的一些元素。于是我们采用双向链表维护这个集合。
设 \(pre(i)\) 表示排序后 \(r_i\) 的前缀,\(suf(i)\) 表示排序后 \(r_i\) 的后缀。在删除元素时,只需要更新指针所指向的元素即可。
当发现 \(\max\{r_{i+1}-r_i\}\le|s|\) 时,输出答案 \(|s|\)。
时间复杂度线性。
代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
const int N = 5e5 + 10;
char str[N];
int n, pi[N], pre[N], suf[N], nxt[N], mx = 1;
vector<int> edge[N];
void Del(int u) {
if (u) {
int s = suf[u], p = pre[u];
mx = max(mx, s - p);
pre[s] = p, suf[p] = s;
}
for (int v: edge[u]) if (v ^ nxt[u]) Del(v);
return;
}
signed main() {
scanf("%s", str + 1);
n = strlen(str + 1);
edge[0].push_back(1);
f(i, 1, n - 1) {
int j = pi[i];
while (j && (str[i + 1] ^ str[j + 1])) j = pi[j];
if (str[i + 1] == str[j + 1]) ++j;
pi[i + 1] = j;
edge[j].push_back(i + 1);
}
for (int i = n; i; i = pi[i]) nxt[pi[i]] = i;
f(i, 1, n) pre[i] = i - 1, suf[i] = i + 1;
Del(0); int ans = nxt[0];
while (mx > ans) Del(ans), ans = nxt[ans];
printf("%d\n", ans);
return 0;
}
H. [POI 2005] Double-row (dwu) (建图)
题意
有 \(2\times n\) 的矩阵,每一个数最多出现两次,一次操作可以交换任意一列的两个数。
现在需要使每一行的数不重复,求最少的操作数。数据保证有解。
\(1\le n\le5\times10^4\),\(1\le x_i,y_i\le10^5\),其中 \(x_i,y_i\) 分别为第一行和第二行的数。
思路
设一个数所在的两个列分别为 \(i\) 和 \(j\),那么:如果这个数在同一行,则 \(i\) 和 \(j\) 交换次数奇偶性不同;如果这个数在不同行,则 \(i\) 和 \(j\) 交换次数奇偶性相同。
我们可以将奇偶性不同转化为连一条边权为 \(1\) 的边,将奇偶性相同转化为连一条边权为 \(0\) 的边,端点为列编号 \(i\) 和 \(j\)。
于是,在一个连通块中,如果一个节点(即一列)的交换次数(\(0/1\))确定了,那么其他所有节点的交换次数也就都确定了。
题目保证有解,所以可以不用管矛盾的情况。
在一个连通块内,这个问题就变成了黑白染色问题,直接搜索即可。\(1\) 边两端颜色不同,\(0\) 边两端颜色相同,黑、白颜色节点数的较小值即为这个连通块的贡献,累加进答案中。
时间复杂度 \(O(n)\)。
代码
#include <iostream>
#include <bitset>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
int n, a[N][2];
pii pos[N];
struct Edge {
int to, nxt, val;
} e[N];
int head[N], cnt;
inline void add(int from, int to, int val) {
e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
e[++cnt].to = from, e[cnt].nxt = head[to], e[cnt].val = val, head[to] = cnt;
return;
}
int ans, sum, siz;
bitset<N> s, vis;
void dfs(int u) {
++siz;
if (s[u]) ++sum;
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].val;
if (vis[v]) continue;
vis[v] = true;
s[v] = s[u] ^ w;
dfs(v);
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
f(i, 1, n) cin >> a[i][0];
f(i, 1, n) cin >> a[i][1];
f(i, 1, n) {
f(j, 0, 1) {
int x;
x = a[i][j];
if (pos[x].first) add(i, pos[x].first, (bool)(pos[x].second == j));
else pos[x] = (pii){i, j};
}
}
f(i, 1, n) {
if (!vis[i]) {
sum = siz = 0;
vis[i] = s[i] = true;
dfs(i);
ans += min(sum, siz - sum);
}
}
cout << ans << '\n';
return 0;
}
I. [POI 2005] The Bus (aut) (树状数组优化 DP)
题意
Byte City 的街道形成了一个标准的棋盘网络——只有北南走向或西东走向。
北南走向的路口从 \(1\) 到 \(n\) 编号,西东走向的路从 \(1\) 到 \(m\) 编号。每个路口用两个数 \((i, j)\) 表示(\(1\le i\le n\),\(1\le j\le m\))。
Byte City 里有一条公交线,在一些路口设置了公交站点。公交车从 \((1, 1)\) 发车,在 \((n, m)\) 结束。公交车只能往北或往东走。
现在有一些乘客在 \(k\) 个站点等车,第 \(i\) 个站点的坐标为 \(x_i,y_i\),等车人数为 \(p_i\)(\(1\le i\le k\))。公交车司机希望在路线中能接到尽量多的乘客。
求最多能接到的乘客数量。
\(1\le n,m\le10^9\),\(1\le k\le10^5\),\(1\le p_i\le10^6\),\(1\le x_i\le n\),\(1\le y_i\le m\),答案不超过 \(10^9\)。
思路
首先将坐标离散化处理。
设 \(f(i)\) 表示到编号为 \(i\) 的站点能接到的最多的乘客数量。那么有转移方程
将站点按 \(x\) 坐标为第一关键字、\(y\) 坐标为第二关键字排序,然后用树状数组维护满足条件的 \(f(j)\) 的最大值,以快速进行转移。
代码
#include <iostream>
#include <unordered_map>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define lowbit(x) (x & (-x))
#define int ll
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m, k, cur, ans, raw[N], cnt;
unordered_map<int, int> val;
struct Node {
int x, y, p;
} pos[N];
int c[N];
void modify(int x, int v) {
while (x <= cnt) {
c[x] = max(c[x], v);
x += lowbit(x);
}
return;
}
int query(int x) {
int res = 0;
while (x >= 1) {
res = max(res, c[x]);
x -= lowbit(x);
}
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
f(i, 1, k) {
cin >> pos[i].x >> pos[i].y >> pos[i].p;
raw[i] = pos[i].y;
}
sort(pos + 1, pos + k + 1, [](Node const &p, Node const &q) { return p.x == q.x ? p.y < q.y : p.x < q.x; });
sort(raw + 1, raw + k + 1);
cnt = unique(raw + 1, raw + k + 1) - raw - 1;
f(i, 1, cnt) val[raw[i]] = i;
f(i, 1, k) {
cur = query(val[pos[i].y]) + pos[i].p;
ans = max(ans, cur);
modify(val[pos[i].y], cur);
}
cout << ans << '\n';
return 0;
}
J. [POI 2006] Professor Szu (pro) (缩点,拓扑排序)
题意
某大学校内有一栋主楼,还有 \(n\) 栋住宅楼。这些楼之间由共 \(m\) 条单向道路连接,但是任意两栋楼之间可能有多条道路,也可能存在起点和终点为同一栋楼的环路。已知任意一栋住宅楼都存在至少一条前往主楼的路线。
现在有一位古怪的教授,他希望每天去主楼上班的路线不同。
一条上班路线中,每栋楼都可以访问任意多次。我们称两条上班路线是不同的,当且仅当两条路线中存在一条路是不同的(两栋楼之间的多条道路被视为是不同的道路)。
现在教授希望知道,从哪些住宅楼前往主楼的上班路线数最多。
输出格式:
- 第一行:如果存在一栋楼到主楼的上班路线数超过了 \(36500\),输出
zawsze。否则输出一个整数,代表从一栋住宅楼前往主楼的最多上班路线数。 - 第二行:输出一个整数 \(p\),代表有多少栋住宅楼能使前往主楼的上班路线数最大化。特别地,如果最大上班路线数超过了 \(36500\),那么这一行请输出能使上班路线数超过 \(36500\) 的住宅楼的数量。
- 第三行:按编号从小到大的顺序输出 \(p\) 个整数,代表能使前往主楼的上班路线最大化的住宅楼的编号。特别地,如果最大上班路线数超过了 \(36500\),那么这一行请输出所有能使上班路线数超过 \(36500\) 的住宅楼的编号。
\(1\le n,m\le10^6\)。
思路
首先,我们建出反图,然后计算从 \(n+1\) 到其他所有点的路径数。
容易发现,如果一个点 \(x\) 属于某个(大小大于 \(1\) 的)SCC 并且这个 SCC 能到达点 \(n+1\),那么点 \(x\) 到 \(n+1\) 的路径数一定为无数个。
所以我们进行 SCC 缩点,然后对缩点后的图进行拓扑排序。
具体地,设 \(f(i)\) 表示从 \(n+1\) 所在的 SCC 到编号为 \(i\) 的 SCC 的路径数。
设 \(n+1\) 所在 SCC 编号为 \(f(x)\),则 \(f(x)=1\)。将所有入度为 \(0\) 的 SCC 入队。
如果编号为 \(i\) 的 SCC 的大小大于 \(1\),并且之前经过过 \(n+1\) 所在的 SCC,那么 \(f(i)\) 为 \(+\infty\)。
否则,\(f(i)\) 为所有能到达 \(i\) 的点 \(j\) 的 \(f(j)\) 的和。
代码
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
const int N = 1e6 + 10;
int n, m, deg[N], f[N];
queue<int> q;
bool ban[N], vis[N];
vector<int> ans;
struct Graph {
struct Edge {
int to, nxt;
} e[N];
int head[N], cnt;
il void add(int from, int to) {
e[++cnt].to = to, e[cnt].nxt = head[from], head[from] = cnt;
return;
}
} a, g;
int dfn[N], low[N], tot;
int sccid[N], c;
bool inscc[N], inStack[N];
int st[N], top;
void Tarjan(int u) {
dfn[u] = low[u] = ++tot;
st[++top] = u, inStack[u] = true;
for (int i = a.head[u]; i; i = a.e[i].nxt) {
int v = a.e[i].to;
if (!dfn[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v])
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u]) {
int t;
ban[++c] = (st[top] != u); //SCC 大小是否大于 1
while (true) {
t = st[top];
inscc[t] = true;
sccid[t] = c;
--top;
inStack[t] = false;
if (t == u) break;
}
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m;
f(i, 1, m) {
int u, v;
cin >> u >> v;
a.add(u, v);
}
f(i, 1, n + 1) if (!dfn[i]) Tarjan(i);
f(i, 1, n + 1) {
for (int k = a.head[i]; k; k = a.e[k].nxt) {
int j = a.e[k].to;
if (i == j) ban[sccid[i]] = true; //自环
else if (sccid[i] != sccid[j])
g.add(sccid[j], sccid[i]), ++deg[sccid[i]]; //缩点后建反图
}
}
int ed = sccid[n + 1];
f(i, 1, c) if (i != ed && !deg[i]) q.push(i); //不包括主楼
while (!q.empty()) { //第一次拓扑排序
int t = q.front();
q.pop();
vis[t] = true;
for (int i = g.head[t]; i; i = g.e[i].nxt) {
int to = g.e[i].to;
if (!--deg[to] && to != ed) q.push(to);
}
}
if (!ban[ed]) q.push(ed), f[ed] = 1;
while (!q.empty()) { //第二次拓扑排序
int t = q.front();
q.pop();
vis[t] = true;
for (int i = g.head[t]; i; i = g.e[i].nxt) {
int to = g.e[i].to;
if (ban[to]) continue;
f[to] = min(36501, f[t] + f[to]);
if (!--deg[to]) q.push(to);
}
}
f(i, 1, n) if (!vis[sccid[i]] || f[sccid[i]] == 36501) ans.push_back(i);
if (!ans.empty()) cout << "zawsze\n";
else {
int mx = 0;
f(i, 1, n) {
if (f[sccid[i]] > mx) mx = f[sccid[i]], ans.clear();
if (f[sccid[i]] == mx) ans.push_back(i);
}
cout << mx << '\n';
}
cout << ans.size() << '\n';
for (auto it: ans) cout << it << ' ';
cout << '\n';
return 0;
}

浙公网安备 33010602011771号