2023-2024 ACM-ICPC Latin American Regional Programming Contest

2023-2024 ACM-ICPC Latin American Regional Programming Contest

C. Candy Rush

题意

给你一个长度为 \(n\) 的数组 \(C\),以及一个种类数量 \(k\),其中 \(1\leq C_i\leq k\),现在你需要找到一个最长的连续子序列,满足

序列每种数出现的次数都一样。

做法

首先我们可以类比 \(k\) 比较小的情况,例如 \(k=2\),那么我们可以设所有满足 \(C_i=1\) 的值为 \(1\),满足 \(C_i=2\) 的值为 \(-1\)

那么题目就 \(\iff\) 求最长的连续子序列,满足 \(\sum_{i=l}^rA_i=0\)

这个显然可以用哈希表 \(+\) 前缀和快速搞定。

那么当 \(k\) 很大的时候,我们应该怎么办,我们也想能不能给这每个种类都设一个值。

\(C_i=1\rightarrow v_1\)
\(C_i=2\rightarrow v_2\)
\(\dots\)
\(C_i=k\rightarrow v_k\)

我们令 \(v_k=-v_1-v_2-...v_{k-1}\)

那么当区间 \([l,r]\) 出现的所有种类出现次数都一样的时候,一定有 \(\sum_{i=l}^rv_i=0\)

当此时这个条件不是充分的,它只是必要的。

这里我们需要引入一个随机化的技巧,我们通过给每一个种类随机一个比较大的值,就可以让我们这个条件在很多情况下都变成充要的。(由于随机的数值范围很大,相当于犯错的概率是很低的)

我们可以利用 \(mt19937\) 给这些数随机一个比较大的值,任何就可以套用我们 \(k=2\) 的做法了。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 4e5 + 10;
LL c[N];
mt19937 mrand(random_device{}());
 
 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n, k;
	cin >> n >> k;
	for (int i = 1; i < k; i++) {
		c[i] = mrand() % 1000000000 + 1;
	}
	c[k] = 0;
	for (int i = 1; i < k; i++) {
		c[k] -= c[i];
	}
	map<LL, int> mp;
	int ans = 0;
	LL pre = 0;
	mp[pre] = 0;
	for (int i = 1; i <= n; i++) {
		int x;
		cin >> x;
		pre += c[x];
		if (mp.count(pre)) {
			ans = max(ans, i - mp[pre]);
		} else mp[pre] = i;
	}
	cout << ans << "\n";
 
	return 0;
}

J. Journey of the Robber

题意

给你 \(n\) 个结点的树,每个点有个权值,\(1\) 号点的权值为 \(1\)\(2\) 号点权值为 \(2\),以此类推。

现在你需要对于每一个点 \(u\),都要求出一个点 \(v\),满足 \(d(u,v)\) 是最小的,且 \(v\) 的权值要大于 \(u\) 的权值,即 \(v>u\)

做法

首先考虑树形 DP 能不能做,显然是不太能做的,换根之后我们并不好维护;在考虑 DSU on tree 好像也不好做,我们需要更新每一个点,但树上启发式合并,我们是不能暴力遍历重儿子的,所以我们无法直接更新重儿子的信息。

那么对于这种和路径相关的,我们就只剩下点分治了,那么我们每次找到重心 \(mid\),相当于问题变成对于每一个点都查询领域,问题显然可以分成经过重心或者不经过,不经过重心的我们直接递归分治即可。经过重心,我们只要把所有点维护到重心的距离,然后按权值排序,直接暴力遍历更新和重心连接的这些子树内所有点的答案,排序之后维护后缀最小值即可。

这里有个值得思考的,就是我们直接全体排序,是会查询到某个点不经过重心的路径,但我们发现这样查询的值是会偏大的,故不会影响真正和 \(u\) 在同一颗子树,不经过重心的路径的答案,所以可以这样做,但如果要求数量,我们就需要特别注意了。

Code

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
vector<int> g[N];
int sz[N], maxs[N], d[N];
bool del[N];
int n;
 
PII ans[N];
 
void solve(int u, int s) {
	int ms = s + 1, root = -1;
	function<void(int, int)> self = [&](int u, int par) {
		sz[u] = 1;
		maxs[u] = 0;
		for (auto v : g[u]) {
			if (del[v] || v == par) continue;
			self(v, u);
			sz[u] += sz[v];
			maxs[u] = max(maxs[u], sz[v]);
		}
		maxs[u] = max(maxs[u], s - sz[u]);
		if (maxs[u] < ms) ms = maxs[u], root = u;
	};
	self(u, -1);
 
	d[root] = 0;
	vector<PII> cur;
	cur.push_back(make_pair(root, 0));
	for (auto v : g[root]) {
		if (del[v]) continue;
		d[v] = 1;
		function<void(int, int)> dfs = [&](int u, int par) {
			sz[u] = 1;
			cur.push_back(make_pair(u, d[u]));
			for (auto v : g[u]) {
				if (del[v] || v == par) continue;
				d[v] = d[u] + 1;
				dfs(v, u);
				sz[u] += sz[v];
			}
		};
		dfs(v, root);	
	}
	
	sort(cur.begin(), cur.end());
	reverse(cur.begin(), cur.end());
 
	PII res = make_pair(n, n);
	for (auto [x, y] : cur) {
		ans[x] = min(ans[x], make_pair(d[x] + res.first, res.second));
		res = min(res, make_pair(y, x));
	}
 
	//分治下去
	del[root] = true;
	for (auto v : g[root]) {
		if (!del[v]) solve(v, sz[v]);
	}
}
 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	
	cin >> n;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for (int i = 1; i <= n; i++) ans[i] = make_pair(n, n);
	solve(1, n);
	for (int i = 1; i <= n; i++) {
		cout << ans[i].second << " \n"[i == n];
	}
 
	return 0;
}

K. Keen on Order

题意

给你一个长度为 \(n\) 的数组 \(V\),和一个 \(k\),其中数组每个元素满足 \(1\leq V_i \leq k\)

现在问你存不存在一个长度为 \(k\) 的排列 \(P\),使得 \(P\) 不是数组 \(n\) 的任何一个子序列。

如果存在,请你输出任意一个满足要求的一个排列,否则请你输出 *

数据范围

\(1\leq n, k\leq 300\)

做法

首先有一个比较明显的贪心做法,在基于子序列的匹配过程,我们可以想到一个如下的贪心手段:

  • 我们假设现在我们整个排列这个子序列匹配到序列中的位置是 \(last\),现在我们需要看看第 \(i\) 位填啥。

  • 如果对于所有没填过的数 \(v\),在 \(last\) 后面找不到,那么显然我们这一位填 \(v\),就可以造成匹配结束。

  • 否则,我们设 \(pos_v\) 表示值为 \(v\)\(last\) 后面第一次出现的位置,我们只要尽可能填 \(pos_v\) 尽可能大的值。

这样看上去是很对的,但是我们不能证明这个做法是充要的,意思是我们这种方法有可能找不到一组解。

例如下面的这个例子:

\(n=10,k=4\),数组 \(V\) 为:\(1,3,2,4,1,3,2,1,3,1\)

显然按照我们的方法,我们会先匹配到 \(4,2,3,1\),然后就无解了。

但我们发现其实 \(2,1,4,3\) 这个排列是一组合法的解。

此时又需要分析了,这也是比较难的部分,当我们难以下手的时候,我们可以尝试数据分治,我们想一下

  1. \(k\) 比较小的时候,我们可以怎么做,我们显然可以暴力状压 DP,我们设 \(dp[i]\) 表示目前已经用的数为 \(i\)

    且按贪心的子序列匹配方法,匹配到的最后一个位置的最大值是多少。

    然后转移 \(dp[mask]=\max_{x\in mask}(next(x,dp[mask-x])\)

    其中 \(next(i,j)\) 表示值为 \(i\)\(j\) 后面第一次出现的位置是多少。

    如果最后转移完 \(dp[(1<<k)-1]>n\),那么说明我们找到了一组合法的排列,倒推回去方案即可。

  2. \(k\) 比较大的时候,我们怎么办,我们就思考我们上面的哪个不太对的贪心做法,我们发现我们假设当且匹配到位置为
    \(pos\),我们第一次填数,至少都会移动 \(k\) 个单位,否则我们就找到一组解了,那么以此类推,我们 \(pos\) 每次至少会移动:\(k+k-1+k-2+...+1\),我们发现当 \(k\ge 25\) 的时候,这个求和式子就 \(>300\),显然是会找到一组解的。

所以我们就得到了数据分治的做法,当 \(k\) 较大的时候,我们就采用上面的贪心做法,较小的时候就暴力状压 DP 即可。

Code

#include <bits/stdc++.h>
using namespace std;


int dp[1 << 24];
pair<int, int> last[1 << 24];
int nxt[310][26], st[26];
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n, k;
    cin >> n >> k;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    {
        vector<int> b(k + 1);
        vector<bool> st(k + 1, false);

        int last = 0;
        for (int i = 1; i <= k; i++) {
            vector<int> suf(k + 1, 0);
            for (int j = last + 1; j <= n; j++) {
                if (!suf[a[j]]) suf[a[j]] = j;
            }
            int pos = 0, ms = 0;
            for (int j = 1; j <= k; j++) {
                if (!st[j] && !suf[j]) {
                    //说明此时填 j 已经起飞了
                    b[i++] = j;
                    st[j] = true;
                    for (int p = 1; p <= k; p++) {
                        if (!st[p]) b[i++] = p;
                    }
                    for(int p = 1; p <= k; p++) {
                        cout << b[p] << " \n"[p == k];
                    }
                    return 0;
                } else if (!st[j]) {
                    if (ms < suf[j]) ms = suf[j], pos = j;
                }
            }
            b[i] = pos, st[pos] = true;
            last = ms;
        }
    }


    for (int i = 1; i <= k; i++) st[i] = n + 1, nxt[n + 1][i] = n + 1;
    for (int i = n; i >= 0; i--) {
        for (int j = 1; j <= k; j++) {
            nxt[i][j] = st[j];
        }
        st[a[i]] = i;
    }

    for (int i = 1; i < 1 << k; i++) {
        for (int j = 0; j < k; j++) {
            if (i >> j & 1) {
                if (dp[i] < nxt[dp[i - (1 << j)]][j + 1]) {
                    dp[i] = nxt[dp[i - (1 << j)]][j + 1];
                    last[i] = {i - (1 << j), j + 1};
                }
            }
        }
    }

    int state = (1 << k) - 1;
    if (dp[state] != n + 1) {
        cout << "*\n";
    } else {
        vector<int> b;
        while (state) {
            auto u = last[state];
            b.push_back(u.second);
            state = u.first;
        }
        for (int i = k - 1; i >= 0; i--) {
            cout << b[i] << " \n"[i == 0];
        }
    }

	return 0;
}
posted @ 2023-11-06 22:19  jackle  阅读(82)  评论(0编辑  收藏  举报