UPC组队训练第十二场补题
One of Each
讲道理这道题应该可做,不过当时太饿了急着吃饭就没心去想了。
用一个栈维护当前解,线性扫描整个序列,对于未取到的数字,我们先检查其是否能在当前解中“上浮”。先说做法,再说原因——检查栈顶元素是否比当前要取数字大,如果大,那么让该数字上浮(当前待取数字最终会压入栈顶,每次检查出栈顶,在所有上浮检查结束后压入栈顶就行),显然字典序最小,重复此操作直至栈为空或栈顶元素不满足上浮条件。上浮的条件除了数字本身的大小关系外,还要看这个栈顶元素在后面是否还会再出现(不然不能让该数字在答案序列中出现在栈顶元素前)。判断后面会不会出现很简单,一开始预处理每个数字出现的个数,线性扫描时不断让对应数字个数减少,即能动态维护当前值后所有数字出现的个数。
如果明白了上述过程,一般就能理解其正确性,不过还是稍作解释:
我们的解一定是在一个子区间中选出来的,那么当我们的区间向右扩大时,若该数字出现过,不用讨论,因为该数字即使在该处取,也没有在之前取到的地方取好(后面能接的数是之前的子集);若该数字没出现过,那么我们要考虑该数字能否在追求字典序最小的情况下优化当前解。根据字典序,当然是越小的数越出现在前面越好,所以我们可以对当前解从后往前检查,如果该数字比后端数字小,那么将它放在该位置显然会使字典序最小,但放的时候也要考虑后面会不会有机会再把这个后端元素取回来,如果取不回来,解最终就凑不齐$k$个了。当我们上浮至无法上浮的位置时,就接着去考虑下一个数的加入就行了。因为我们每次上浮都保证了去掉的后端元素存在于后面未取数字中,所以一次线性扫描一定能恰巧取出字典序最小的$k$个元素形成的序列。
之前听说stack的实现都是基于vector的,推荐直接用vector替代stack,那就用vector吧。
#include <bits/stdc++.h>
using namespace std;
int n, k;
vector<int> v, cnt, ans;
vector<bool> book;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
cin >> n >> k;
v.resize(n), cnt.resize(k + 1), book.resize(k + 1);
for (int &it:v) {
cin >> it;
cnt[it]++;
}
for (int i = 0; i < n; i++) {
cnt[v[i]]--;
if (!book[v[i]]) {
book[v[i]] = true;
while (!ans.empty() && cnt[ans.back()] > 0 && ans.back() > v[i])
book[ans.back()] = false, ans.pop_back();
ans.push_back(v[i]);
}
}
for (int i = 0; i < k - 1; i++) cout << ans[i] << ' ';
cout << ans.back() << endl;
return 0;
}
Swap Free
初看题目,这道题有个很裸的图论模型,即可以单词为点,为互不可一步转换的词之间建无向边,则题目待求即可转换为最大完全子图的节点数(最大团问题)。
不过写的时候我没接触过最大团问题,甚至都没听说过“最大团”这个词,想来惭愧……
不过脑补一下,还是能想到最大团的一个求法。当时想的是二分最大团的节点数,然后把度数不足节点数的“枝叶”删掉,但很快就被我想反例,不过这其中删枝叶的思想还是有一定意义的。
这里需要用到补图的思想——既然在原图中团的点之间两两连边,那么在补图中他们两两不连边,成一个独立集。那么补图的最大独立集即对应原图的最大团(这么一想可能我在接触最大独立集时就没有研究清楚)。
那么考虑本问题的补图是什么——以单词为点,为互相可转换的词之间建无向边。这个图有一个特别的性质,若存在边$(a,b)$和边$(a,c)$,则必定不会存在边$(b,c)$,具体来解释,其实每条边之间都有一个隐形的边权,即两串相互转换对应的字母下标对。那么设$(a,b)$权值为$(p_1,p_2)$,$(a,c)$权值为$(p_3,p_4)$,$(b,c)$权值为$(p_5,p_6)$。这也进一步意味着$a[p_1]=b[p_2]$,$a[p_3]=c[p_4]$,假设存在边$(b,c)$,则有$b[p_5]=c[p_6]$。写到这里,读者可能认为这些式子间毫无关系,但注意本题有一个隐含条件,即对于有边相连的两个单词,除了这些错位不相同的字母对,其它部分应是位置一一对应的相等字符,即有$a[p_5]=b[p_5]=c[p_6]$,对下标的相等情况稍作讨论,即可得出假设矛盾。
清楚了这样一个性质,该图即为一个二分图,本来在一般图中求最大独立集很复杂,但在二分图中就非常简单了。在二分图中,最大独立集=点数-最大匹配数,最大匹配数可以使用匈牙利算法解决。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> Edge;
int n;
vector<string> words;
vector<Edge> edges;
vector<int> heads;
vector<bool> vis;
vector<int> match;
inline bool check(const string &s1, const string &s2) {
// 如果两串恰好有两位置不同,即需连边
int cnt = 0;
for (int i = 0; i < s1.size(); i++) {
if (s1[i] != s2[i]) {
cnt++;
if (cnt > 2) return false;
}
}
return cnt != 0;
}
inline void addEdge(int u, int v) {
edges.emplace_back(v, heads[u]);
heads[u] = edges.size() - 1;
}
bool dfs(int u) {
for (int i = heads[u], v; ~i; i = edges[i].second) {
if (!vis[v = edges[i].first]) {
vis[v] = true;
if (!match[v] || dfs(match[v])) {
match[v] = u;
return true;
}
}
}
return false;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
cin >> n;
heads.resize(n, -1);
vis.resize(n);
match.resize(n);
words.resize(n);
for (string &s:words) cin >> s;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (check(words[i], words[j]))
addEdge(i, j), addEdge(j, i);
int max_match = 0;
for (int i = 0; i < words.size(); i++) {
fill(vis.begin(), vis.end(), false);
vis[i] = true;
if (dfs(i)) max_match++;
}
cout << n - max_match / 2 << endl; // 对所有点dfs,结果要除2
return 0;
}