跳到主要内容

第 194 场周赛

Problem A - 数组异或操作

模拟。

参考代码(C++)
class Solution {
public:
int xorOperation(int n, int start) {
int ans = 0;
for (int i = 0; i < n; ++i)
ans ^= (start + 2 * i);
return ans;
}
};

Problem B - 保证文件名唯一

按要求进行模拟。

参考代码(C++)
class Solution {
public:
vector<string> getFolderNames(vector<string>& names) {
unordered_set<string> s;
unordered_map<string, int> dict;
vector<string> ans;
for (string name : names) {
if (!s.count(name)) {
ans.emplace_back(name);
s.insert(name);
dict[name] = 1;
} else {
int idx = dict[name];
while (s.count(name + "(" + to_string(idx) + ")"))
idx++;
string t = name + "(" + to_string(idx) + ")";
s.insert(t);
ans.emplace_back(t);
dict[name] = idx + 1;
dict[t] = 1;
}
}
return ans;
}
};

Problem C - 避免洪水泛滥

首先一次遍历找到每一个下雨天下雨的池塘上一次下雨的日子。然后开始从最后一天倒推:

  • 如果这一天下雨,那么需要检查下雨的池塘是否为空。
    • 如果不为空,说明有无法解决的冲突,无解。
    • 如果为空,那么将其置为满。同时,如果这一池塘之前还下过雨,就把之前下雨的记录加入大根堆中,等待消除矛盾。
  • 如果这一天不下雨,并且大根堆非空(有需要消除的矛盾),就取出堆顶元素(最靠近的一个下雨天),将对应的池塘排空。

如果最后大根堆非空,代表还有未消除的矛盾,无解。

总时间复杂度为O(nlogn)O(n\log n)

参考代码(C++)
class Solution {
public:
vector<int> avoidFlood(vector<int>& rains) {
int n = rains.size();
vector<int> ans(n);
unordered_map<int, int> pos;
vector<int> last(n, -1);
unordered_map<int, bool> full;
for (int i = 0; i < n; ++i) {
if (rains[i] > 0) {
if (pos.count(rains[i]))
last[i] = pos[rains[i]];
pos[rains[i]] = i;
}
}
priority_queue<pair<int, int>> pq;
for (int i = n - 1; i >= 0; --i) {
if (rains[i] > 0) {
if (full[rains[i]])
return {};
ans[i] = -1;
full[rains[i]] = true;
if (last[i] == -1)
continue;
else
pq.push({last[i], rains[i]});
} else {
if (pq.empty())
ans[i] = 1;
else {
pair<int, int> top = pq.top();
pq.pop();
ans[i] = top.second;
full[top.second] = false;
}
}
}
if (!pq.empty())
return {};
return ans;
}
};

Problem D - 找到最小生成树里的关键边和伪关键边

本题与CF160D - Edges in MST相同。

方法一 暴力枚举

首先用Kruskal算法进行一次常规的MST,得到最小权值。

对于每条边,再进行两次额外的MST:

  • 强制从该边开始MST(直接将该边加入生成树的边集):如果最后的权值大于最小权值,说明该边既不是关键边,也不是非关键边
  • 删除该边后再MST:如果最后的权值大于最小权值,或者图不连通,说明该边为关键边;否则该边可能为非关键边(结合上面一次的结果可以最终判断该边是否是非关键边)。

该方法的时间复杂度为O(E2)O(E^2),在本题的数据范围内可以通过。

参考代码(C++)
struct Edge {
int u, v, w, idx;

bool operator<(const Edge &other) const {
return w < other.w || (w == other.w && u < other.u) || (w == other.w && u == other.u && v < other.v);
}
};

class UnionFind {
int n;
vector<int> parent, size;

public:
UnionFind(int n) {
this->n = n;
parent = vector<int>(n);
size = vector<int>(n, 1);
for (int i = 0; i < n; ++i)
parent[i] = i;
}

int find(int idx) {
if (parent[idx] == idx)
return idx;
return parent[idx] = find(parent[idx]);
}

bool is_connected(int a, int b) {
return find(a) == find(b);
}

void connect(int a, int b) {
int fa = find(a), fb = find(b);
if (fa != fb) {
if (size[fa] > size[fb]) {
parent[fb] = fa;
size[fa] += size[fb];
} else {
parent[fa] = fb;
size[fb] += size[fa];
}
}
}

int components() {
vector<bool> is_root(n);
for (int i = 0; i < n; ++i)
is_root[find(i)] = true;
int ans = 0;
for (int i = 0; i < n; ++i)
ans += is_root[i];
return ans;
}
};

class Solution {
int n;
vector<Edge> e;
vector<vector<int>> edges;

int MST(int prioritize, int ignore) {
UnionFind uf(n);
int ans = 0;
if (prioritize != -1) {
vector<int> v = edges[prioritize];
uf.connect(v[0], v[1]);
ans += v[2];
}
for (Edge ei : e) {
if (ei.idx == prioritize || ei.idx == ignore)
continue;
if (!uf.is_connected(ei.u, ei.v)) {
uf.connect(ei.u, ei.v);
ans += ei.w;
}
}
if (uf.components() > 1)
return INT_MAX;
return ans;
}

public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>> &edges) {
this->n = n;
this->edges = edges;
for (int i = 0; i < edges.size(); ++i)
e.emplace_back(Edge{edges[i][0], edges[i][1], edges[i][2], i});
sort(e.begin(), e.end());
int mst = MST(-1, -1);
vector<int> critical, non_critical;
for (int i = 0; i < edges.size(); ++i) {
int with = MST(i, -1);
int without = MST(-1, i);
if (with > mst)
continue;
if (without > mst)
critical.emplace_back(i);
else
non_critical.emplace_back(i);
}
return {critical, non_critical};
}
};

方法二 Tarjan+并查集

维护一个所有顶点的并查集。将所有边按权值升序排序,然后将权值相同的边作为一组,从权值最小的组开始逐组处理。

每一组处理过程中,如果某条边的两个顶点已经在同一连通分量中,该边一定不在任何最小生成树中。否则该边至少是在一个最小生成树中,也即至少为非关键边。

接下来,将这些边加入邻接表构成新的图,然后在新的图上使用Tarjan算法找出所有的桥边。桥边一定是关键边。

最后,合并新连通的连通分量,也即“缩点”。

运行过程的瓶颈是排序,所以总时间复杂度为O(ElogE)O(E\log E)

参考代码(C++)
#define INF 0x3f3f3f3f

struct Edge {
int from, to, value;

Edge(int from, int to, int value) : from(from), to(to), value(value) {}
};

class Graph {
int n, idx;
vector<Edge> edges;
vector<vector<int>> adj;
vector<int> dfn, low, ans, parent, size;
vector<bool> vis;

int find(int a) { return parent[a] == a ? a : parent[a] = find(parent[a]); }

void connect(int a, int b) {
int fa = find(a), fb = find(b);
if (fa != fb) {
if (size[fa] > size[fb]) {
parent[fb] = fa;
size[fa] += size[fb];
} else {
parent[fa] = fb;
size[fb] += size[fa];
}
}
}

void tarjan(int u) {
dfn[u] = low[u] = ++idx;
for (int ei : adj[u]) {
if (vis[ei])
continue;
vis[ei] = true;
Edge &e = edges[ei];
int cv = find(e.from) + find(e.to) - u;
int v = find(cv);
if (!dfn[v]) {
tarjan(v);
if (dfn[u] < low[v])
ans[ei] = 0;
}
low[u] = min(low[u], low[v]);
}
}

public:
explicit Graph(int n) : n(n) {
adj = vector<vector<int >>(n);
dfn = vector<int>(n, -1);
low = vector<int>(n, INF);
parent = vector<int>(n);
size = vector<int>(n, 1);
for (int i = 0; i < n; ++i)
parent[i] = i;
}

void add_edge(int u, int v, int w) {
adj[u].emplace_back(edges.size());
adj[v].emplace_back(edges.size());
edges.emplace_back(u, v, w);
}

vector<vector<int>> solve() {
int m = edges.size();
vector<int> order(m);
for (int i = 0; i < m; ++i)
order[i] = i;
sort(order.begin(), order.end(),
[&](int i, int j) { return edges[i].value < edges[j].value; });
ans = vector<int>(m, -1);
vis = vector<bool>(m);
for (int start = 0; start < m; ++start) {
int end = start;

// 将相同权值的边归为一组进行处理
while (end + 1 < m &&
edges[order[end + 1]].value == edges[order[start]].value)
end++;

// 清除处理上一组时连的边
for (int k = start; k <= end; ++k) {
auto[u, v, w] = edges[order[k]];
int fu = find(u), fv = find(v);
adj[fu].clear();
adj[fv].clear();
dfn[fu] = dfn[fv] = 0;
}

// 连上新边
// 如果该边的两个端点已经在一个连通分量中,该边一定不在最小生成树中
for (int k = start; k <= end; ++k) {
auto[u, v, w] = edges[order[k]];
int fu = find(u), fv = find(v);
if (fu != fv) {
adj[fu].emplace_back(order[k]);
adj[fv].emplace_back(order[k]);
ans[order[k]] = 1;
}
}

// Tarjan算法找出桥边
// 桥边是关键边,其余非桥边是非关键边
idx = 0;
for (int k = start; k <= end; ++k) {
auto[u, v, w] = edges[order[k]];
int fu = find(u), fv = find(v);
if (!dfn[fu])
tarjan(fu);
if (!dfn[fv])
tarjan(fv);
}

// 合并连通分量,也就是所谓的“缩点”
for (int k = start; k <= end; ++k) {
auto[u, v, w] = edges[order[k]];
connect(u, v);
}
start = end;
}

vector<vector<int>> ret(2);
for (int i = 0; i < m; ++i)
if (~ans[i]) ret[ans[i]].emplace_back(i);
return ret;
}
};

class Solution {
public:
vector<vector<int>> findCriticalAndPseudoCriticalEdges(int n, vector<vector<int>> &edges) {
Graph g(n);
for (auto v : edges)
g.add_edge(v[0], v[1], v[2]);
return g.solve();
}
};