图论口胡记录

图论口胡记录

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;
}
posted @ 2023-11-13 15:51  Katyusha_Lzh  阅读(71)  评论(2)    收藏  举报