图论口胡记录
图论口胡记录
Xor-MST
\(Borvuka\)算法版题
\(Borvuka\)的流程是每次对于每个联通块中都找到一条向外部最小的边,然后再将边相连的两个连通块合并。可以发现每次连通块的个数都会减半,只会合并\(\log_n\)次,那么中间找最小边的过程中,对于没有显式建边的题目我们就可以用数据结构维护求出最小边。总时间复杂度为\(O(n\log_n\times DS)\)。
对于这道题,我们考虑用\(0-1trie\)快速计算异或最小值。记合并过程中同一个连通块的点同色,那么可以先将所有节点值加入\(trie\)中,当找某个连通块向外的最小边时就将\(trie\)中所有该种颜色的点删除查最值,然后再插回去即可。复杂度\(O(n log_n^2)\)。
ll boruvka(){
dsu.init(n);
for (int i=1;i<=n;i++){
col[i]=i;
t.insert(a[i],1,i);
}
cnt=0;ans=0;
while(cnt<n-1){
for (int i=1;i<=n;i++) v[i].clear();
for (int i=1;i<=n;i++){
v[col[i]].push_back(i);
}
for (int i=1;i<=n;i++){
if (v[i].size()){
mn[i]=0x3f3f3f3f;
for (const auto &x:v[i]) t.insert(a[x],-1,x);
for (const auto &x:v[i]){
pair <int,int> p=t.query(a[x]);
if (p.first<mn[i]){
out[i]=make_pair(x,p.second);
mn[i]=p.first;
}
}
for (const auto &x:v[i]) t.insert(a[x],1,x);
}
}
for (int i=1;i<=n;i++){
if (v[i].size()){
int u=out[i].first,v=out[i].second;
int X=dsu.find(u),Y=dsu.find(v);
if (X==Y) continue;
if (dsu.siz[X]>dsu.siz[Y]) swap(X,Y);
cnt++;ans+=a[u]^a[v];
dsu.fa[X]=Y;dsu.siz[Y]+=dsu.siz[X];
}
}
for (int i=1;i<=n;i++) col[i]=dsu.find(i);
}
return ans;
}
Tree MST这道题也可以用\(Brovuka\)做。
P3645 [APIO2015] 雅加达的摩天楼
有一个很明显的暴力:对于每个节点每次向左/右跳到达的点连边,跑一遍\(b_0-b_1\)的最短路就是答案。
考虑优化这个算法,可以想到每个点只向\(b_i-p_i\),\(b_i+p_i\)连边。然而无法保证正确性,因为中间办法更换\(doge\),只能建n张子图,每个点都向左右\(i\)的位置连边,空间时间都吃不消。
又因为类比弹飞绵羊当步长比较大时,暴力跳复杂度是正确的,所以考虑根号分治。设分治阀域为\(len\),建出\(1-len\)的子图。对于\(p_i\)大于\(len\)的点直接暴力连边,小于\(len\)的点将其与\(p_i\)的子图上对应节点连边再跑最短路即可。
for (int i=1;i<=len;i++){
for (int j=1;j<=n;j++){
if (j-i>=1) add(n+(i-1)*n+j,n+(i-1)*n+j-i,1);
if (j+i<=n) add(n+(i-1)*n+j,n+(i-1)*n+j+i,1);
add(n+(i-1)*n+j,j,0);
}
}
for (int i=1;i<=n;i++){
for (const auto &x:vec[i]){
if (p[x]>len){
for (int j=i-p[x],stp=1;j>=1;j-=p[x],stp++) add(i,j,stp);
for (int j=i+p[x],stp=1;j<=n;j+=p[x],stp++) add(i,j,stp);
}
else add(i,n+(p[x]-1)*n+i,0);
}
}
分析\(len\)取何值时复杂度最优,子图会建\(3*len*n\)条边,暴力会连\(\frac{n^2}{len}\)条边,由均值有当\(len\)取\(\sqrt{\frac{n}{3}}\)时最优。
Dynamic Shortest Path
每次暴力更新后重新跑最短路复杂度\(O(qnlogm)\)比较接近时间限制,考虑优化使得每次不重跑最短路将\(log\)优化掉。
首先第一次\(dij\)跑出最短路后,先暴力将每条更新边加一,再记录下每个点最短路的改变量\(delta_i\),显然有\(delta_i<=min(n-1,v)\),因此考虑类似于最短路的松弛操作,开\(min(n-1,v)\)个\(queue\)记录\(delta_i\),每次取出一个节点\(x\),用\(dis[x]-dis[y]+w'+delta[x]\)(即当前节点改变量和前驱传过来的变化量之和)最小值尝试更新邻接点\(delta\)。最后再将每个节点\(dis\)加上\(delta_i\)即可。
for (int i=1;i<=n;i++) delta[i]=1e9;
for (int i=1;i<=v;i++){
int x;read(x);
E[x].w++;
}
delta[1]=0;vec[0].push(1);
for (int i=0;i<=min(n-1,v);i++){
while(vec[i].size()){
int x=vec[i].front();vec[i].pop();
if (delta[x]!=i) continue;
for (int j=head[x];j;j=E[j].nxt){
int y=E[j].to,w=E[j].w;
int tmp=w+dis[x]-dis[y]+delta[x];
if (tmp<delta[y]&&tmp<=min(n-1,v)){
delta[y]=tmp;
vec[tmp].push(y);
}
}
}
}
for (int i=1;i<=n;i++){
if (delta[i]!=1e9) dis[i]+=delta[i];
}
CF1473E Minimum Path
妙妙题。
有一个比较显然的暴力是对于每个节点都用\(m^2\)个状态,即设\(dis(u,l,r)\)为在\(u\)点进过边权最小为\(l\),最大为\(r\)时的最短路,直接跑\(dij\)。
但是这样会有很多冗余的状态,考虑优化。先思考一个弱化的问题:求免费和加倍的边权在这条路径上任意分别取一条的最短路。显然这两条边一定分别是最大边和最小边,因此这个问题和原问题是等价的,这就是这道题比较神仙的思想。
那么接下来考虑对等价问题设计优化的状态,设\(dis(u,0/1,0/1)\)为到\(u\)且最大/最小边选/未选的最短路长度。讨论同状态之间和从0变为1的转移即可,比较简单,不再赘述。这玩意的本质是拆点。
注意最后的答案是\(min(dis[i][1][1],dis[i][0][0])\) (因为可能有只走一条边的情况,但是在等价问题中这条边会被选两次)
跳蚤王国的宰相
首先可以求出原树的重心\(C\)。
考虑对于每个不为\(C\)的节点\(x\)计算答案,发现从\(x->C\)的路径是这样的。

由于\(C\)是重心,所以\(x\)子树一定大小之和小于\(n/2\),只需要考虑将图中的一部分边断开接到\(x\)下边。
显然这样的边不可能在\(x->C\)的路径上,因为C子树大小大于等于\(n/2\),因此只能在\(C\)子树内。
考虑将\(C\)所有儿子按子树大小排序,每次贪心地选取子树大小最大,正确性显然。
然后讨论割完后\(x->C\)的路径节点数+\(C\)剩余子树大小\(\leq\) \(n/2\)已经满足条件,和\(C\)剩余子树大小\(\leq\) \(n/2\)后再多割一刀将\(C\)整棵子树割下来到\(x\)上两种情况。
实现上前缀和加二分即可。
for (int i=1;i<=n;i++){
if (i==rt){puts("0");continue;}
int s=n-siz[bel[i]],delta=s-n/2;
if (delta==0) {puts("0");continue;}
int L=0,R=(int)vec.size()-1,p=-1,P;
while(L<=R){
int mid=(L+R)>>1;
int cur=sum[mid];
if (mid>=pos[bel[i]]) cur-=siz[bel[i]];
if (n-siz[i]-cur<=n/2) P=mid,p=mid,R=mid-1;
else if (s-cur<=n/2) P=mid,p=mid+1,R=mid-1;
else L=mid+1;
}
if (P>=pos[bel[i]]) p--;
printf("%d\n",p+1);
}
模拟赛题目 水淹七军
题意:对于一个无向图的边重定向,求定向后形成的有向图中最长路径的最小值。\(n \leq 16\)。
假设我们已经将这张图定向得到了一张有向图,那么考虑按照每条边从起点到终点连边形成一张分层图,显然分层图中层数相同的两点不会存在连边,否则其中一个点就不属于这一层。同时最长的路径就是 层数-1
那么问题可以转化为重定向生成的分层图中层数最小值。发现\(n\) 的值很状压。设 \(dp_{mask}\) 为 \(mask\) 中的点形成的层数最小值, 可以由枚举子集得到新增的这一层的点转移过来,即 \(dp_{mask} = \min_{curmask \in mask} dp_{mask \oplus curmask} + 1\),注意 \(curmask\) 中没有相互连边,预处理即可。
点击查看代码
#include <bits/stdc++.h>
void solve() {
int n, m;
std::cin >> n >> m;
std::vector <std::pair<int, int> > edge(m + 1);
std::vector <std::vector <bool> > G(n + 1);
for (int i = 0; i <= n; i++) G[i].resize(n + 1, 0);
for (int i = 1; i <= m; i++) {
std::cin >> edge[i].first >> edge[i].second;
edge[i].first--;edge[i].second--;
G[edge[i].first][edge[i].second] = 1;
G[edge[i].second][edge[i].first] = 1;
}
int up = (1 << n);
std::vector <bool> g(up + 1, 1);
for (int mask = 0; mask < up; mask++) {
std::vector <int> v;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) v.push_back(i);
}
for (int i = 0; i < v.size(); i++) {
for (int j = i + 1; j < v.size(); j++) {
int x = v[i], y = v[j];
if (G[x][y]) g[mask] = 0;
}
}
}
const int INF = 0x3f3f3f3f;
std::vector <int> dp(up + 1, INF), pre(up + 1, -1);
dp[0] = 0;
for (int mask = 1; mask < up; mask++) {
for (int curmask = mask; curmask; curmask = (curmask - 1) & mask) {
if (!g[curmask]) continue;
if (dp[mask ^ curmask] + 1 < dp[mask]) {
dp[mask] = dp[mask ^ curmask] + 1;
pre[mask] = curmask ^ mask;
}
}
}
std::vector <int> dep(n + 1);
int cnt = dp[up - 1];
auto dfs = [&](auto self, int mask) {
if (mask == 0) return;
for (int i = 0; i < n; i++) {
if (!(pre[mask] & (1 << i)) && (mask & (1 << i))) dep[i] = cnt;
}
cnt--;
self(self, pre[mask]);
};
dfs(dfs, up - 1);
std::cout << dp[up - 1] - 1 << "\n";
for (int i = 1; i <= m; i++) {
int x = edge[i].first, y = edge[i].second;
if (dep[x] > dep[y]) std::swap(x, y);
x++;y++;
std::cout << x << " " << y << "\n";
}
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
[CCO2021] Travelling Merchant
\(tag\):拓扑排序,\(dp\),贪心,消除后效性
妙妙题
比较显然的\(dp\)是设\(dp_i\)为从\(i\)出发遍历整个图最小初始钱数,显然有\(dp_u = \min (\max(dp_v - p_{u,v}, r_{u,v}))\),但是有环。
因此考虑以一定的顺序转移来消除后效性。先发掘一些特殊的点的性质。发现若一个点的出度为\(0\),那么这个点的一定无解;若一条边\(r\)值为所有边的最大值,那么有\(r\)元从这个点出发一定是一个可行的解(不一定最小)。
先将边按\(r\)从大到小排序,发现可以拓扑排序,队列中记录所有\(dp\)值确定的点\(v\)。考虑边\((u, v, r, p)\),其中\(dp_v\)的值已经确定,那么可以直接用上文的式子更新\(dp_u\),并且将这条边打标记删去,若此后\(u\)出度为\(0\),则将\(u\)入队。若排序后当前这条边被删除,那么说明这个点在删除一些边的图上可以通过某些边到达出度为\(0\)的点,反之则不能,由于当前的\(r\)是未被删的边中\(r\)最大的,不会受到其他边\(r\)的限制,所以直接用\(r\)更新当前点答案并删去这条边,尝试入队即可。
点击查看代码
#include <bits/stdc++.h>
void solve() {
int n, m;
std::cin >> n >> m;
struct Edge {
int to, r, p, nxt;
};
std::vector <Edge> E(m + 1);
std::vector <int> head(n + 1);
int ecnt = 0;
auto add = [&](int u, int v, int r, int p) {
E[++ecnt].to = v;
E[ecnt].nxt = head[u];
head[u] = ecnt;
E[ecnt].r = r;
E[ecnt].p = p;
};
struct edge {
int u, v, r, p, id;
bool operator < (const edge &x) const {
return r > x.r;
}
};
std::vector <edge> e(m + 1);
std::vector <int> deg(n + 1);
for (int i = 1; i <= m; i++) {
int a, b, r, p;
std::cin >> a >> b >> r >> p;
e[i].u = a, e[i].v = b, e[i].r = r, e[i].p = p;
e[i].id = i;
add(b, a, r, p);
deg[a]++;
}
std::sort(e.begin() + 1, e.end());
const int INF = 0x3f3f3f3f;
std::vector <int> dp(n + 1, INF);
std::vector <bool> vis(m + 1);
std::queue <int> q;
for (int i = 1; i <= n; i++) {
if (!deg[i]) q.push(i);
}
for (int i = 1; i <= m; i++) {
while (q.size()) {
int v = q.front(); q.pop();
for (int j = head[v]; j; j = E[j].nxt) {
if (vis[j]) continue;
vis[j] = 1;
int u = E[j].to, p = E[j].p, r = E[j].r;
if (dp[v] != INF) dp[u] = std::min(dp[u], std::max(r, dp[v] - p));
deg[u]--;
if (!deg[u]) q.push(u);
}
}
if (!vis[e[i].id]) {
vis[e[i].id] = 1;
dp[e[i].u] = std::min(dp[e[i].u], e[i].r);
deg[e[i].u]--;
if (!deg[e[i].u]) q.push(e[i].u);
}
}
for (int i = 1; i <= n; i++) {
if (dp[i] != INF) std::cout << dp[i] << " ";
else std::cout << -1 << " ";
}
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
Lost Array
\(tag\):消除后效性
先思考最少步数。首先如果\(n\)很小可以考虑状压/搜索,由于本题中每个位置除去值以外都是等价的,因此考虑直接设\(dp_x\)表示求出\(x\)个数的异或和最少步数。
转移时可以每次枚举\(i\)表示新选的\(k\)个数中存在于\(x\)中的数的个数,显然有\(i \in [0, \min (k,x ) ]\)并且 \((k - i) \leq (n - x)\),设\(y = x - i +(k - i)\),那么有 \(dp_y = \min{dp_x + 1}\)。
发现转移具有后效性,可以建图\(bfs\)跑最短路即可。
然后再说方案,\(bfs\)时记录前驱并按照新选的点个数直接构造就行了。
点击查看代码
#include <bits/stdc++.h>
void solve() {
int n, k;
std::cin >> n >> k;
const int INF = 0x3f3f3f3f;
std::vector <int> dis(n + 1, INF), ine(n + 1), pre(n + 1, -1);
dis[0] = 0;
std::queue <int> q;
q.push(0);
while (q.size()) {
int u = q.front(); q.pop();
for (int i = 0; i <= std::min(u, k); i++) {
if (k - i <= n - u) {
int v = u - i + (k - i);
if (!(0 <= v && v <= n)) continue;
if (dis[v] > dis[u] + 1) {
dis[v] = dis[u] + 1;
ine[v] = i;
pre[v] = u;
q.push(v);
}
}
}
}
if (dis[n] >= INF) {
std::cout << "-1" << "\n";
return;
}
int ans = 0;
std::vector <bool> vis(n + 1);
auto dfs = [&](auto self, int u) -> void {
if (pre[u] == -1) return;
else self(self, pre[u]);
int v = pre[u], i = ine[u];
std::cout << "? ";
int cnt = 0;
std::vector <int> tmp;
for (int j = 1; j <= n && cnt < i; j++) {
if (vis[j]) {
++cnt;
tmp.push_back(j);
std::cout << j << " ";
}
}
cnt = 0;
for (int j = 1; j <= n && cnt < k - i; j++) {
if (!vis[j]) {
++cnt;
tmp.push_back(j);
std::cout << j << " ";
}
}
for (const auto &x : tmp) vis[x] = vis[x] ^ 1;
std::cout.flush();
int res;
std::cin >> res;
ans ^= res;
};
dfs(dfs, n);
std::cout << "! ";
std::cout << ans << "\n";
std::cout.flush();
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
AT_joisc2014_a バス通学
\(tag\):减少冗余信息。
好久没有见到这样能让我眼前一亮的题了
首先发现答案随着 \(l\) 的增加显然是单调不减的。
先考虑暴力,如果正向建图跑没有很好的办法确定1号点的起始时间,只能二分检验。但是\(n\)号点的结束时间已知,可以直接建反图逆推得到1号点的最晚时间,复杂度\(O(nm)\),需要进一步优化。
可以尝试利用答案的单调性。具体来说,将询问离线下来并按\(l\)从小到大排序,将随着\(l\)的增大,一些边会变得不优,对答案此后都没有贡献。考虑以一定的顺序使得每条边贡献只被计算一次,发现将反图的每条边按\(y_i\)从小到大排序后,因为\(l\)单调递增,每次每个点用上的边集只可能是一段连续的前缀区间。于是可以每次重新计算答案时只加入新增的边即可,实现上类似弧优化。复杂度\(O(m\log m + m)\),瓶颈在于排序。
点击查看代码
#include <bits/stdc++.h>
struct Edge {
int a, b, x, y;
bool operator < (const Edge &e) const {
if (y != e.y) return y < e.y;
return x < e.x;
}
};
void solve() {
int n, m;
std::cin >> n >> m;
std::vector <Edge> adj(m + 1);
for (int i = 1; i <= m; i++) {
std::cin >> adj[i].a >> adj[i].b >> adj[i].x >> adj[i].y;
}
std::sort(adj.begin() + 1, adj.begin() + m + 1);
std::vector <std::vector<int> > e(n + 1);
for (int i = 1; i <= m; i++) {
e[adj[i].b].push_back(i);
}
std::vector <int> cur(n + 1), ans(n + 1, -1);
auto dfs = [&](auto self, int u) -> void {
for (int &i = cur[u]; i < e[u].size(); ) {
int edg = e[u][i];
if (adj[edg].y <= ans[u]) {
ans[adj[edg].a] = std::max(ans[adj[edg].a], adj[edg].x);
i++;
self(self, adj[edg].a);
}
else break;
}
};
int qry;
std::cin >> qry;
std::vector <int> prt(qry + 1);
std::vector <std::pair<int, int> > q(qry + 1);
for (int i = 1; i <= qry; i++) {
std::cin >> q[i].first;
q[i].second = i;
}
std::sort(q.begin() + 1, q.begin() + qry + 1);
for (int i = 1; i <= qry; i++) {
ans[n] = q[i].first;
dfs(dfs, n);
prt[q[i].second] = ans[1];
}
for (int i = 1; i <= qry; i++) {
std::cout << prt[i] << "\n";
}
}
int main() {
// std::ios::sync_with_stdio(0);
// std::cin.tie(0);
// std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}

浙公网安备 33010602011771号