POI - B

POI - B

A. [POI 2005] Cash Dispenser (bank) (思维题)

SZKOpuł | 洛谷 P3417 | 码创未来

题意

有一个 \(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) (贪心)

SZKOpuł | 洛谷 P3419 | 码创未来

题意

两个正整数集 \(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) (并查集)

SZKOpuł | 洛谷 P3420 | 码创未来

题意

\(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) (单调队列)

SZKOpuł | 洛谷 P3422 | 码创未来

题意

\(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)\),满足

\[p_i+p_{i+1}+\dots+p_j<d_i+d_{i+1}+\dots+d_j, \]

从而使 \(j\) 到不了 \(j+1\)。也就是说,如果位置 \(i\) 可行,那么对于所有 \(j\in[i,i+n)\),必须满足

\[\sum_{k=i}^{j}\,p_k-d_k\ge0. \]

于是设前缀和 \(s_i=\sum\limits_{j=1}^ip_j-d_j\),则上式变为 \(s_j-s_{i-1}\ge0\;(\forall\,j\in[i,i+n-1])\),即

\[\min_{i\le j\le i+n-1}\{s_j\}\ge s_{i-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}\)。则上式变为

\[\max_{i-n+1\le j\le i}\{s_{j-1}\}\le s_i. \]

为了方便,我们将 \(i\) 替换为 \(i+1\),将 \(j\) 替换为 \(j+1\),上式变为

\[\max_{i-n+1\le j\le i}\{s_j\}\le s_{i+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) (多重背包优化)

SZKOpuł | 洛谷 P3423 | 码创未来

题意

一共有 \(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 表示法)

SZKOpuł | 洛谷 P3424 | 码创未来

题意

\(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) (失配树,双向链表)

SZKOpuł | 洛谷 P3426 | 码创未来

题意

你打算在纸上印一串字母。

为了完成这项工作,你决定刻一个印章。印章每使用一次,就会将印章上的所有字母印到纸上。

同一个位置的相同字符可以被印多次。求出印章上的字符串的最小长度。

例如:用 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) (建图)

SZKOpuł | 洛谷 P3430 | 码创未来

题意

\(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)

SZKOpuł | 洛谷 P3431 | 码创未来

题意

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\) 的站点能接到的最多的乘客数量。那么有转移方程

\[f(i)=\max\{f(j)\}+p_i\qquad\mathrm{where\ }x_j\le x_i\mathrm{\ and\ }y_j\le y_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) (缩点,拓扑排序)

SZKOpuł | 洛谷 P3436 | 码创未来

题意

某大学校内有一栋主楼,还有 \(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;
}
posted @ 2023-03-23 22:07  f2021ljh  阅读(62)  评论(0)    收藏  举报