Codeforces - 图论题目(难度:2000)

欢迎访问本菜鸡的独立博客:Codecho

Summary: 感觉只有 1 4 \frac{1}{4} 41 的题目比较硬核,能学到些东西;剩下的题目比较水,以 DFS 为主。

463D - Gargari and Permutations (建图 + 拓扑序上dp)

1. 题意

给你 k k k 个长度为 n n n排列,问它们的最长公共子序列的长度。

数据范围: 1 ≤ n ≤ 1 0 3 ; 2 ≤ k ≤ 5 1 \le n \le 10^3; 2 \le k \le 5 1n103;2k5

2. 解题思路

设排列 i i i 中第 j j j 个数字为 p i , j p_{i,j} pi,j,则对于每个数字,将所有的有向边 < p i , j , p i , k > ( j < k ≤ n ) <p_{i,j}, p_{i,k}>(j < k \le n) <pi,j,pi,k>(j<kn)边权均加 1 1 1

之后,我们得到了一个 DAG,很快想到可以在拓扑序上面进行 dp

很显然,只有边权为 k k k 的边是有用的,我们根据这个条件先对整个图进行一遍拓扑排序,把拓扑序存储到 t o p o ​ topo​ topo 数组中。

d p [ i ] ​ dp[i]​ dp[i] 为以 i ​ i​ i 为起点的 LCS 的长度。

之后,我们枚举 t o p o ​ topo​ topo 数组,对于 v = t o p o [ i ] ​ v=topo[i]​ v=topo[i],将其作为临时的终点,之后从 1 ​ 1​ 1 n ​ n​ n 枚举起点 u ​ u​ u,若 < u , v > ​ <u,v>​ <u,v> 的边权为 k ​ k​ k ,表明 d p [ v ] ​ dp[v]​ dp[v] 可以由 d p [ u ] ​ dp[u]​ dp[u] 转移过来。

遍历完之后,所有的 d p ​ dp​ dp 值都已经计算完成,其中的最大值便是答案。

时间复杂度: O ( k ⋅ n 2 ) ​ O(k \cdot n^2)​ O(kn2)

3. 代码

int a[10][maxn], head[maxn], n, k;
int G[maxn][maxn], indeg[maxn], topo[maxn], dp[maxn];

queue<int> que;
int cnt = 0;
void toposort()
{
    for(int i = 1; i <= n; i++)
    {
        if(!indeg[i])   que.push(i);
    }
    while(!que.empty())
    {
        int now = que.front();
        que.pop();
        topo[++cnt] = now;
        for(int i = 1; i <= n; i++)
        {
            if(G[now][i] != k)  continue;
            --indeg[i];
            if(!indeg[i])   que.push(i);
        }
    }
}

int main()
{
    scanf("%d%d", &n, &k);

    for(int i = 1; i <= k; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            scanf("%d", &a[i][j]);
        }
    }


    for(int i = 1; i <= k; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            for(int l = j + 1; l <= n; l++)
            {
                G[a[i][j]][a[i][l]]++;
            }
        }
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(G[i][j] == k)    indeg[j]++;
        }
    }

    toposort();

    for(int i = 1; i <= cnt; i++)
    {
        for(int u = 1; u <= n; u++)
        {
            int v = topo[i];
            if(G[u][v] == k)
            {
                dp[v] = max(dp[v], dp[u] + 1);
            }
        }
    }


    int ans = 0;
    for(int i = 1; i <= n; i++) ans = max(ans, dp[i]);

    printf("%d\n", ans + 1);
    return 0;
}

449B - Jzzhu and Cities (Dijkstra 松弛条件改造)

1. 题意

给你一个有 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 个节点的无向图 1 1 1 为其源点。

现在有两种边

( 1 ) ​ (1)​ (1) m ( 1 ≤ m ≤ 3 ⋅ 1 0 5 ) ​ m(1 \le m \le 3 \cdot 10^5)​ m(1m3105) 条道路,连接城市 u i ​ u_i​ ui v i ​ v_i​ vi,长度为 x i ​ x_i​ xi

( 2 ) (2) (2) k ( 1 ≤ k ≤ 1 0 5 ) k(1 \le k \le 10^5) k(1k105) 条铁路,连接城市 1 1 1 x i x_i xi,长度为 y i y_i yi

最多删掉多少条铁路,使得 1 1 1 号城市到每个城市的最短路径的长度不变。

图保证连通,存在重边,不存在自环。

2. 解题思路

存边的时候,多存一个参数 t y p e type type,表示边的类型, 0 0 0 为道路, 1 1 1 为铁路。

f a v fa_v fav 为松弛 v v v 的边的 t y p e type type d v d_v dv 1 1 1 v v v 的最短路径的长度。

对于边 ( u , v , w ) (u,v,w) (u,v,w),有两个松弛条件

( 1 ) (1) (1) d v > d u + w d_v > d_u + w dv>du+w,则按照原来的思路进行松弛,并将 v v v 加入到堆中。此时更新 f a v fa_v fav 为当前边的 t y p e type type

( 2 ) (2) (2) d v = d u + w d_v = d_u + w dv=du+w,则让类型为 0 0 0 的边替换类型为 1 1 1 的边,更新 f a v fa_v fav 0 0 0

跑完 Dijkstra 之后,统计一下 f a v fa_v fav 1 1 1 的点的个数 n u m num num,则答案为 k − n u m k-num knum

时间复杂度: O ( ( n + m + k ) log ⁡ n ) ​ O((n+m+k)\log n)​ O((n+m+k)logn)

3. 代码

int n, m, k, cnt;
int head[maxn], fa[maxn];
ll d[maxn];
bool vis[maxn];

struct edge
{
    int v, nxt, type;
    ll w;
} Edge[8 * maxn];

struct node
{
    ll d;
    int id;
    node(ll _d, int _id):
        d(_d), id(_id) {}
    const bool operator < (const node b) const
    {
        return d > b.d;
    }
};

void init()
{
    for(int i = 0; i <= n; i++)
    {
        head[i] = -1;
        fa[i] = -1;
    }
    cnt = 0;
}

void addedge(int u, int v, ll w, int type)
{
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].type = type;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

priority_queue<node> que;

void Dijkstra()
{
    memset(d, 0x3f, sizeof(d));
    d[1] = 0;
    que.push(node(d[1], 1));
    while(!que.empty())
    {
        node now = que.top();
        que.pop();
        if(vis[now.id]) continue;
        vis[now.id] = true;
        for(int i = head[now.id]; i != -1; i = Edge[i].nxt)
        {
            int v = Edge[i].v;
            ll w = Edge[i].w;
            if(d[v] > d[now.id] + w)
            {
                d[v] = d[now.id] + w;
                fa[v] = Edge[i].type;
                que.push(node(d[v], v));
            }
            else if(d[v] == d[now.id] + w)
            {
                if(Edge[i].type == 0)
                {
                    que.push(node(d[v], v));
                    fa[v] = 0;
                }
            }
        }
    }
}

int main()
{
    int u, v;
    ll w;
    scanf("%d%d%d", &n, &m, &k);

    init();

    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%lld", &u, &v, &w);
        addedge(u, v, w, 0);
        addedge(v, u, w, 0);
    }

    for(int i = 1; i <= k; i++)
    {
        scanf("%d%lld", &v, &w);
        addedge(1, v, w, 1);
        addedge(v, 1, w, 1);
    }

    Dijkstra();

    int ans = 0;
    for(int i = 2; i <= n; i++) ans += fa[i];

    printf("%d\n", k - ans);
    return 0;
}

558C - Amr and Chemistry (建图 + 换根法)

1. 题意

给你 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1n105) 个数 a i ( 1 ≤ a i ≤ 1 0 5 ) a_i(1 \le a_i \le 10^5) ai(1ai105),对于每个数,可对其进行若干次如下操作之一:

( 1 ) (1) (1) a i ← 2 ⋅ a i a_i \leftarrow 2 \cdot a_i ai2ai

( 2 ) (2) (2) a i ← ⌊ a i 2 ⌋ a_i \leftarrow \lfloor \frac{a_i}{2} \rfloor ai2ai

最少的操作次数,使得所有数字一样。

2. 解题思路

我们可以通过建立一个有向图来解决该问题。

设有向边 < u , v > <u,v> <u,v> 表示数 v v v 可以通过一步操作转化为数 u u u

按照这种思路,对于数 a ​ a​ a,可以建立有向边 < ⌊ a 2 ⌋ , a > ​ <\lfloor \frac{a}{2} \rfloor, a>​ <2a,a> < a , 2 ⋅ a > ​ <a, 2 \cdot a>​ <a,2a>

建边的时候不要重复建边或者超范围 ( [ 1 , 1 0 5 ] [1,10^5] [1,105]) 建边 。

因为序列 a a a 中可能出现重复数字,因此需要记录每个数字 i i i 的出现次数 w i w_i wi

假设 1 ​ 1​ 1 为根,设 d p u ​ dp_u​ dpu 表示以 u ​ u​ u 为根的子树上面出现的数字(即在序列 a ​ a​ a 中出现的数字)的个数。

d e p t h i ​ depth_i​ depthi 为点 i ​ i​ i 所在的深度,设 d e p t h 1 = 0 ​ depth_1=0​ depth1=0

我们可以通过一遍 DFS 求出所有的 d p u dp_u dpu d e p t h i depth_i depthi

a n s i ans_i ansi 表示将所有的数字最终化为 i i i 所需的最小步数。

则有
a n s 1 = ∑ i = 2 1000 w i ⋅ d e p t h i ans_1=\sum_{i=2}^{1000}{w_i\cdot depth_i} ans1=i=21000widepthi
基于 a n s 1 ans_1 ans1,我们可以再进行一次 DFS,利用换根法,计算出所有的 a n s i ans_i ansi

设当前遍历到点 i d id id 和边 < i d , v > <id,v> <id,v>,则有
a n s v = a n s i d − 1 ⋅ d p v + 1 ⋅ ( d p 1 − d p v ) ans_v=ans_{id}- 1 \cdot dp_v+ 1 \cdot (dp_1-dp_v) ansv=ansid1dpv+1(dp1dpv)
这个式子的意思是,将 v v v 及其子树中点的深度均减去 1 1 1,将其他点的深度均加上 1 1 1,即得到了 d p v dp_v dpv

但是这里需要注意的是,奇数 i i i整棵树的树根当且仅当 d p i = n dp_i=n dpi=n其他奇数都不可以做整棵树的树根

因为若 d p i < n dp_i<n dpi<n,表明有不在 i i i 子树中的数字,因而涉及到从小到大的转换。

而从小到大的转换无法将一个数转化为奇数

因此,需要对这一情况进行特判

最终的答案为
min ⁡ 1 ≤ i ≤ 1 0 5 { a n s i } \min_{1 \le i \le 10^5}\left\{ans_i\right\} 1i105min{ansi}
时间复杂度: O ( 1 0 5 log ⁡ 1 0 5 ) O(10^5 \log 10^5) O(105log105)

3. 代码

int a[maxn], head[maxn], n, cnt;
ll dp[maxn], w[maxn], depth[maxn];
ll ans[maxn];

struct edge
{
    int v, nxt;
} Edge[4 * maxn];

void init()
{
    for(int i = 0; i <= 100000; i++) head[i] = -1;
    cnt = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void dfs(int id)
{
    dp[id] = w[id];
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        depth[v] = depth[id] + 1;
        dfs(v);
        dp[id] += dp[v];
    }
}

void dfs2(int id)
{
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v % 2 == 1 && dp[v] != n)    ans[v] = 0x3f3f3f3f;
        else ans[v] = ans[id] - dp[v] + (dp[1] - dp[v]);
        dfs2(v);
    }
}

int main()
{
    scanf("%d", &n);

    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        w[a[i]]++;
    }
    init();

    for(int i = 1; i <= 100000; i++)
    {
        if(i * 2 <= 100000)
        {
            addedge(i, i * 2);
        }
        if(i / 2 >= 1)
        {
            if(i % 2 == 1)  addedge(i / 2, i);
        }
    }

    dfs(1);
    for(int i = 2; i <= 100000; i++)    ans[1] += w[i] * depth[i];
    dfs2(1);

    ll res = 0x3f3f3f3f3f3f3f3f;
    for(int i = 1; i <= 100000; i++)
    {
        res = min(res, ans[i]);
    }

    printf("%lld\n", res);
    return 0;
}

739B - Alyona and a tree (树上差分)

1. 题意

给你一棵 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \le n \le 2 \cdot 10^5) n(1n2105) 个节点的无向树,根节点为 1 1 1

对于每个点 i i i,都有一个权值 a i ( 1 ≤ a i ≤ 1 0 9 ) a_i(1 \le a_i \le 10^9) ai(1ai109)

d i s t ( u , v ) dist(u,v) dist(u,v) 表示 u u u v v v 路径上面点的权值和。

u u u 控制点 v ( u ≠ v ) v(u \ne v) v(u̸=v) 当且仅当 v ∈ s u b t r e e ( u ) v \in subtree(u) vsubtree(u) 并且 d i s t ( u , v ) ≤ a v dist(u,v) \le a_v dist(u,v)av

问每个点可以控制多少个点。

2. 解题思路

我们能够发现,在 DFS 的过程中,我们既可以认为在处理点 i d id id,也可以认为在处理点 i d id id 所在的链

如果正向思考的话,我们需要处理每个点能够控制的点。

但是,对于本题,我们可以反向思考:对于每个点,处理能够控制它的所有节点

u u u 能够控制 v v v,则显然 u u u v v v 在一条链上,有
d i s t ( u , v ) = d i s t ( 1 , v ) − d i s t ( 1 , u ) dist(u,v) = dist(1,v) - dist(1,u) dist(u,v)=dist(1,v)dist(1,u)

d i s t ( u , v ) ≤ a v ⇔   d i s t ( 1 , v ) − d i s t ( 1 , u ) ≤ a v ⇔   d i s t ( 1 , v ) − a v ≤ d i s t ( 1 , u ) \begin{aligned} &dist(u,v) \le a_v \\ \Leftrightarrow\text{ }& dist(1,v)-dist(1,u) \le a_v \\ \Leftrightarrow\text{ }& dist(1,v)-a_v \le dist(1,u) \end{aligned}   dist(u,v)avdist(1,v)dist(1,u)avdist(1,v)avdist(1,u)

如果一个一个处理的话,时间复杂度显然过高,因此我们需要采用差分的思想。

d i f f i diff_i diffi 表示点 i i i 对应的差分值。

很显然,既然要进行差分,我们就要进行两遍 DFS:

第一遍 DFS 用于求出所有的 d i f f i ​ diff_i​ diffi,第二遍 DFS 用于求 d i f f i ​ diff_i​ diffi 的前缀和,即每个点对应的答案。

第二遍 DFS 很容易进行,这里不对此进行叙述,直接看第一遍 DFS。

设当前遍历到的节点为 i d ​ id​ id,当前深度为 d e p t h ​ depth​ depth,指向当前点的边权为 l e n ​ len​ len

f a t h e r i father_i fatheri 表示点 i i i 的父节点, i d d d idd_d iddd 表示当前链上深度为 d d d 的节点编号, p r e f i x d e p t h prefix_{depth} prefixdepth 表示当前链上截止到 d e p t h depth depth 深度的点的权值和.

根据上面的不等关系,我们可以利用 lower_bound 找到第一个大于等于 p r e f i x d e p t h − a i d prefix_{depth}-a_{id} prefixdepthaid p r e f i x d prefix_d prefixd,则 d d d 为当前链上能够控制 i d id id最远的点的深度。

因此整个链上,深度 d d d 所对应的点 i d id id 所经过的所有点都可以控制点 i d id id,根据差分的思想,我们进行下列操作:

diff[id]++;
diff[father[idd[pos]]]--;

因此,第一遍 DFS 结束之后,所有的 d i f f i diff_i diffi 便都被计算出来。

对于每个点 i i i,最终的答案为 d i f f i − 1 diff_i-1 diffi1(因为要把自身排除掉)。

时间复杂度: O ( n log ⁡ n ) ​ O(n \log n)​ O(nlogn)

3. 代码

int n, cnt, head[maxn];
ll a[maxn], prefix[maxn], diff[maxn];
int father[maxn], idd[maxn];

struct edge
{
    int v, nxt;
    ll w;
} Edge[2 * maxn];

void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}

void addedge(int u, int v, ll w)
{
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void dfs(int id, int fa, int depth, ll len)
{
    father[id] = fa;
    idd[depth] = id;
    prefix[depth] = prefix[depth - 1] + len;
    int pos = lower_bound(prefix + 1, prefix + depth + 1, prefix[depth] - a[id]) - prefix;
    diff[id]++;
    diff[father[idd[pos]]]--;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa)     continue;
        dfs(v, id, depth + 1, Edge[i].w);
    }
}

void dfs2(int id, int fa)
{
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa) continue;
        dfs2(v, id);
        diff[id] += diff[v];
    }
}

int main()
{
    int u, v;
    ll w;
    scanf("%d", &n);
    init();
    for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);

    for(int v = 2; v <= n; v++)
    {
        scanf("%d%lld", &u, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }

    dfs(1, 0, 1, 0);
    dfs2(1, 0);

    for(int i = 1; i <= n; i++)
    {
        if(i > 1)   printf(" ");
        printf("%lld", diff[i] - 1);
    }
    return 0;
}

842C - Ilya And The Tree (DFS + 暴力枚举)

1. 题意

给你一棵 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \le n \le 2 \cdot 10^5) n(1n2105) 个节点,根节点为 1 1 1 的无向树。每个点 i i i 都有一个权值 a i ( 1 ≤ a i ≤ 2 ⋅ 1 0 5 ) a_i(1 \le a_i \le 2 \cdot 10^5) ai(1ai2105)

b e a u t y ( x ) beauty(x) beauty(x) 为从根节点到 x x x 的路径上所有点的最大公约数,且 b e a u t y ( 1 ) = a 1 beauty(1) = a_1 beauty(1)=a1

现在你可以选择一个点,将其权值置为 0 0 0

对于每个点 x x x,问其 b e a u t y ( x ) ​ beauty(x)​ beauty(x) 可能的最大值。

对于每个点,须独立进行考虑。

2. 解题思路

对于两个正整数 a a a b b b,满足
gcd ( a , b ) ∣ a gcd ( a , b ) ∣ b \text{gcd}(a,b) | a \\ \text{gcd}(a,b) | b gcd(a,b)agcd(a,b)b
而每个数的因子个数为 log ⁡ \log log 级别的。

而在 DFS 的时候,我们只需考虑当前节点所在的链。

因此,我们可以利用 set 存储当前链上所有可能出现的 gcd \text{gcd} gcd,对整棵树进行一遍 DFS 即可。

时间复杂度: O ( n ⋅ log ⁡ 2 n ) O(n \cdot \log^2 n) O(nlog2n)

3. 代码

int a[maxn], head[maxn], n, cnt;
set<int> se[maxn];
int ans[maxn];

struct edge
{
    int v, nxt;
} Edge[2 * maxn];

void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

int gcd(int a, int b)
{
    return b == 0 ? a : gcd(b, a % b);
}

void dfs(int id, int fa, int GCD)
{
    set<int> :: iterator iter;
    iter = se[id].end();
    --iter;
    ans[id] = max((*iter), GCD);
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa) continue;
        for(iter = se[id].begin(); iter != se[id].end(); ++iter)
        {
            int val = *iter;
            se[v].insert(gcd(val, a[v]));
        }
        se[v].insert(gcd(GCD, a[v]));
        dfs(v, id, gcd(GCD, a[id]));
    }
}

int main()
{
    int u, v;
    scanf("%d", &n);
    init();

    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for(int i = 1; i <= n - 1; i++)
    {
        scanf("%d%d", &u, &v);
        addedge(u, v);
        addedge(v, u);
    }

    se[1].insert(0);
    se[1].insert(a[1]);
    dfs(1, -1, 0);

    for(int i = 1; i <= n; i++)
    {
        if(i > 1)   printf(" ");
        printf("%d", ans[i]);
    }
    return 0;
}

767C - Garland (DFS + 思维)

1. 题意

给你一棵有 n ( 3 ≤ n ≤ 1 0 6 ) n(3 \le n \le 10^6) n(3n106) 个节点的无向有根树,对于点 i i i,有权值 t i ( − 100 ≤ t i ≤ 100 ) t_i(-100 \le t_i \le 100) ti(100ti100)

现在问你是否有一种方法,将整棵树剪成三棵,使得每颗树的权值相同

t r e e ( u ) tree(u) tree(u) 的权值 w u w_u wu ∑ v ∈ t r e e ( u ) t v \sum_{v \in tree(u)}{t_v} vtree(u)tv

2. 解题思路

设根节点为 r o o t root root

看完题目,我们会很自然地想到剪完之后,每棵树的权值为 w r o o t 3 ​ \frac{w_{root}}{3}​ 3wroot

最初,我想到的是枚举两棵上述权值的子树,并将其删掉。但后来发现我忽略了子树套子树的情况。

考虑了所有情况后,我们对 DFS 进行改造,当发现一个权值为 w r o o t 3 \frac{w_{root}}{3} 3wroot 的子树 u u u(树根不能为 r o o t root root)之后, u u u 的权值置为 0 0 0,这样可以直接枚举下一棵子树,并且将所有的情况都考虑到了。

时间复杂度: O ( n ) ​ O(n)​ O(n)

3. 代码

int n, cnt, sum, root, tot;
int head[maxn], t[maxn], dp[maxn], ans[3];

struct edge
{
    int v, nxt;
} Edge[2 * maxn];

void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
    root = -1;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void dfs(int id, int fa)
{
    dp[id] = t[id];
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa) continue;
        dfs(v, id);
        dp[id] += dp[v];
    }
    if(dp[id] * 3 == sum && id != root)
    {
        if(tot >= 2)    return;
        dp[id] = 0;
        ans[++tot] = id;
    }
}

int main()
{
    int u;
    scanf("%d", &n);
    init();

    for(int v = 1; v <= n; v++)
    {
        scanf("%d%d", &u, &t[v]);
        sum += t[v];
        if(!u)
        {
            if(root != -1)
            {
                printf("-1\n");
                return 0;
            }
            else root = v;
            continue;
        }
        addedge(u, v);
        addedge(v, u);
    }

    if(sum % 3)
    {
        printf("-1\n");
        return 0;
    }

    dfs(root, -1);

    if(tot <= 1)    printf("-1\n");
    else printf("%d %d\n", ans[1], ans[2]);
    return 0;
}

780D - Innokenty and a Football League (2-SAT)

1. 题意

现在有 n ( 1 ≤ n ≤ 1 0 3 ) n(1 \le n \le 10^3) n(1n103) 支球队,每支球队 i i i 有两个名字 n a m e i , 0 name_{i,0} namei,0 n a m e i , 1 name_{i,1} namei,1。每个球队必须在两个名字中选择其中一个,并且满足如下条件:

( 1 ) (1) (1) 对于球队 i i i,若其选择了 n a m e i , 1 name_{i, 1} namei,1,则对于任意的 j ( 1 ≤ j ≤ n , i ≠ j ) j(1 \le j \le n,i \ne j) j(1jn,i̸=j),若满足 n a m e i , 0 ≠ n a m e j , 0 name_{i,0} \ne name_{j,0} namei,0̸=namej,0,则不可选择 n a m e j , 0 name_{j,0} namej,0

( 2 ) (2) (2) 不能有任意两支球队的名字相同。

问每支球队的取名方案。

2. 解题思路

本题为 2-SAT \text{2-SAT} 2-SAT 问题,需要建立一个 2 n 2n 2n 个点的有向图

i , j ∈ [ 1 , n ] , i ≠ j i,j \in [1,n], i \ne j i,j[1,n],i̸=j

则设点 i , j i,j i,j 分别表示 i i i 选择了 n a m e i , 0 name_{i,0} namei,0 j j j 选择了 n a m e j , 0 name_{j,0} namej,0

设点 i + n , j + n ​ i + n,j + n​ i+n,j+n 分别表示 i ​ i​ i 选择了 n a m e i , 1 ​ name_{i,1}​ namei,1 j ​ j​ j 选择了 n a m e j , 1 ​ name_{j,1}​ namej,1

设有向边 < i , j > <i,j> <i,j> 表示 i i i 选择了 n a m e i , 0 name_{i,0} namei,0 j j j 必须选择 n a m e j , 0 name_{j,0} namej,0

其他情况同理

我们可以在 [ 1 , n ] [1,n] [1,n] 范围内遍历 i i i j ( i ≠ j ) j(i \ne j) j(i̸=j),根据题中条件有以下三种情况

( 1 ) (1) (1) n a m e i , 0 = n a m e j , 0 name_{i,0}=name_{j,0} namei,0=namej,0,则添加如下边:

< i , j + n > <i,j+n> <i,j+n> 及其逆否命题 < j , i + n > <j, i+n> <j,i+n>(条件 ( 1 ) (1) (1)

< i + n , j + n > <i+n, j+n> <i+n,j+n> 及其逆否命题 < j , i > <j, i> <j,i>(条件 ( 2 ) (2) (2)

( 2 ) ​ (2)​ (2) n a m e i , 1 = n a m e j , 1 ​ name_{i,1}=name_{j,1}​ namei,1=namej,1,则添加如下边:

< i + n , j > <i+n, j> <i+n,j> 及其逆否命题 < j + n , i > <j+n, i> <j+n,i>(条件 ( 1 ) (1) (1)

( 3 ) ​ (3)​ (3) n a m e i , 0 = n a m e j , 1 ​ name_{i,0}=name_{j,1}​ namei,0=namej,1,则添加如下边:

< i , j > <i, j> <i,j> 及其逆否命题 < j + n , i + n > <j+n, i+n> <j+n,i+n>(条件 ( 1 ) (1) (1)

建边之后,在整张图上面跑 Tarjan,求出所有的强连通分量

之后,判断每个 i ( 1 ≤ i ≤ n ) i(1 \le i \le n) i(1in) 是否满足 i i i i + n i + n i+n 在同一个强连通分量中,若满足,则表明若选择 n a m e i , 0 name_{i,0} namei,0,则必须选择 n a m e i , 1 name_{i,1} namei,1,这与题意矛盾,因此答案不存在。

否则,输出一种合法的答案。

时间复杂度: O ( 2 n ) O(2n) O(2n)

3. 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn], rev[maxn], var[maxn];
bool instack[maxn];

struct edge
{
    int v, nxt;
} Edge[8 * maxn * maxn];

void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}

string name[maxn][2];

int main()
{
    string stra, strb;
    scanf("%d", &n);
    init();

    for(int i = 1; i <= n; i++)
    {
        cin >> stra >> strb;
        name[i][0] += stra[0];
        name[i][0] += stra[1];
        name[i][0] += stra[2];
        name[i][1] += stra[0];
        name[i][1] += stra[1];
        name[i][1] += strb[0];
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(i == j)  continue;
            if(name[i][0] == name[j][0])
            {
                addedge(i, j + n);
                addedge(j, i + n);
                addedge(i + n, j + n);
                addedge(j, i);
            }
            if(name[i][1] == name[j][1])
            {
                addedge(i + n, j);
                addedge(j + n, i);
            }
            if(name[i][0] == name[j][1])
            {
                addedge(i, j);
                addedge(j + n, i + n);
            }
        }
    }

    for(int i = 1; i <= 2 * n; i++)
    {
        if(!dfn[i]) tarjan(i);
    }

    bool isok = true;
    for(int i = 1; i <= n; i++)
    {
        if(col[i] == col[i + n])
        {
            isok = false;
            break;
        }
        rev[i] = i + n;
        rev[i + n] = i;
    }

    if(!isok)
    {
        printf("NO\n");
        return 0;
    }

    for(int i = 1; i <= 2 * n; i++)
    {
        var[i] = col[i] > col[rev[i]];
    }

    printf("YES\n");
    for(int i = 1; i <= n; i++)
    {
        cout << name[i][var[i]] << endl;
    }
    return 0;
}

776D - The Door Problem (2-SAT)

1. 题意

给你 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 扇门和 m ( 2 ≤ m ≤ 1 0 5 ) m(2 \le m \le 10^5) m(2m105) 个开关,每扇门恰好由两个开关控制,每个开关控制着若干个门。

对于门 i ​ i​ i,若 r i = 0 ​ r_i=0​ ri=0 表明初始状态下门是锁着的,若 r i = 1 ​ r_i=1​ ri=1 表明初始状态下门是打开的。

触动开关会使其控制的每一扇门的状态取反

问是否可以通过某种策略(即选择某些开关将其打开),使得所有的门都是打开的。

2. 解题思路

本题为 2-SAT \text{2-SAT} 2-SAT 问题。

设点 j ( 1 ≤ j ≤ m ) j(1 \le j \le m) j(1jm) 表示开关 j j j 处于关闭(初始)状态,设点 j + m j+m j+m 表示开关 j j j 处于开启状态。

考虑每扇门 i i i,有以下两种情况

( 1 ) (1) (1) r i = 0 r_i=0 ri=0,则控制这扇门的两个开关必须且只能开启其中一个

因此建立有向边 < i + m , j > <i+m, j> <i+m,j> < j , i + m > <j, i+m> <j,i+m> 以及它们的逆否命题 < j + m , i > <j+m, i> <j+m,i> < i , j + m > ​ <i, j + m>​ <i,j+m>

( 2 ) (2) (2) r i = 1 r_i=1 ri=1,则控制这扇门的两个开关要么全部打开,要么全部关闭

因此建立有向边 < i + m , j + m > <i+m, j+m> <i+m,j+m> < i , j > <i, j> <i,j> 以及它们的逆否命题 < j , i > <j, i> <j,i> < j + m , i + m > <j+m, i+m> <j+m,i+m>

图建完之后,跑一遍 2-SAT \text{2-SAT} 2-SAT 即可。

时间复杂度: O ( 2 m ) O(2m) O(2m)

3. 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn];
bool instack[maxn];

int r[maxn];

struct edge
{
    int v, nxt;
} Edge[8 * maxn];

void init()
{
    for(int i = 0; i <= 2 * m; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}

vector<int> ve[maxn];

int main()
{
    int num, v;
    scanf("%d%d", &n, &m);
    init();

    for(int i = 1; i <= n; i++) scanf("%d", &r[i]);

    for(int i = 1; i <= m; i++)
    {
        scanf("%d", &num);
        for(int j = 1; j <= num; j++)
        {
            scanf("%d", &v);
            ve[v].pb(i);
        }
    }

    for(int i = 1; i <= n; i++)
    {
        int a = ve[i][0], b = ve[i][1];
        if(r[i])
        {
            addedge(a + m, b + m);
            addedge(a, b);
            addedge(b + m, a + m);
            addedge(b, a);
        }
        else
        {
            addedge(a + m, b);
            addedge(a, b + m);
            addedge(b, a + m);
            addedge(b + m, a);
        }
    }

    for(int i = 1; i <= 2 * m; i++)
    {
        if(!dfn[i]) tarjan(i);
    }

    bool isok = true;
    for(int i = 1; i <= m; i++)
    {
        if(col[i] == col[i + m])
        {
            isok = false;
            break;
        }
    }

    if(isok)    printf("YES\n");
    else printf("NO\n");
    return 0;
}

550D - Regular Bridge (数学 + 建图)

1. 题意

构造一个每个点的度数恰好为 k ( 1 ≤ k ≤ 100 ) k(1 \le k \le 100) k(1k100),且至少有一个桥的无向图。

规定点数 n n n 和边数 m m m 均不能超过 1 0 6 10^6 106

2. 解题思路

**握手定理:**设一个无向连通图 G G G 的点数为 n n n,每个点 i i i 的度数为 d i d_i di,边数为 m m m,则满足
∑ x ∈ G d x = 2 ⋅ m \sum_{x \in G}d_x=2\cdot m xGdx=2m
设要求出的图的边双连通分量的数量为 b b b,则桥的数量为 b − 1 b-1 b1

设每个边双连通分量 i i i 的点数为 n i n_i ni,边数为 m i m_i mi,则根据握手定理,我们可以列出如下方程
k ( n 1 − 1 ) + ( k − 1 ) = 2 m 1 k ( n 2 − 1 ) + 2 ( k − 1 ) = 2 m 2 k ( n 3 − 1 ) + 2 ( k − 1 ) = 2 m 3 . . . k ( n b − 1 − 1 ) + 2 ( k − 1 ) = 2 m b − 1 k ( n b − 1 ) + ( k − 1 ) = 2 m b \begin{aligned} k(n_1-1)+(k-1)&=2m_1 \\ k(n_2-1)+2(k-1)&=2m_2 \\ k(n_3-1)+2(k-1)&=2m_3 \\ ...& \\ k(n_{b-1}-1)+2(k-1)&=2m_{b-1} \\ k(n_b-1)+(k-1)&=2m_b \end{aligned} k(n11)+(k1)k(n21)+2(k1)k(n31)+2(k1)...k(nb11)+2(k1)k(nb1)+(k1)=2m1=2m2=2m3=2mb1=2mb
整理得
k n 1 = 2 m 1 + 1 k n 2 = 2 m 2 + 2 k n 3 = 2 m 3 + 2 . . . k b − 1 = 2 m b − 1 + 2 k n b = 2 m b + 1 \begin{aligned} kn_1&=2m_1+1 \\ kn_2&=2m_2+2 \\ kn_3&=2m_3+2 \\ &... \\ k_{b-1}&=2m_{b-1}+2 \\ kn_b&=2m_b+1 \end{aligned} kn1kn2kn3kb1knb=2m1+1=2m2+2=2m3+2...=2mb1+2=2mb+1
因此,若 k k k偶数,则上述方程无法成立,即没有答案。

为了简单起见,我们设 b = 2 b=2 b=2

因此,我们有
k n 1 = 2 m 1 + 1 k n 2 = 2 m 2 + 1 \begin{aligned} kn_1&=2m_1+1 \\ kn_2&=2m_2+1 \end{aligned} kn1kn2=2m1+1=2m2+1
下列条件天然满足:
m 1 = k n 1 − 1 2 ≤ ( n 1 − 1 ) ⋅ n 1 2 m_1=\frac{kn_1-1}{2}\le \frac{(n_1-1)\cdot n_1}{2} m1=2kn112(n11)n1

m 2 = k n 2 − 1 2 ≤ ( n 2 − 1 ) ⋅ n 2 2 m_2=\frac{kn_2-1}{2}\le \frac{(n_2-1)\cdot n_2}{2} m2=2kn212(n21)n2

所以,我们可以暴力枚举 n 1 n_1 n1,求出合适的 m 1 m_1 m1

n 2 = n 1 n_2=n_1 n2=n1 m 2 = m 1 m_2=m_1 m2=m1

因此, n = n 1 + n 2 ​ n=n_1+n_2​ n=n1+n2 m = m 1 + m 2 + 1 ​ m=m_1+m_2+1​ m=m1+m2+1

建图策略如代码所示,这里不再赘述。

3. 代码

int deg[205];
bool G[205][205];
int n;

void printedge(int u, int v)
{
    printf("%d %d\n", u, v);
    printf("%d %d\n", u + n, v + n);
    G[u][v] = G[v][u] = true;
    G[u + n][v + n] = G[v + n][u + n] = true;
    deg[u]++;
    deg[v]++;
    deg[u + n]++;
    deg[v + n]++;
}

int main()
{
    int k;
    scanf("%d", &k);

    if(k == 1)
    {
        printf("YES\n");
        printf("2 1\n");
        printf("1 2\n");
        return 0;
    }

    if(k % 2 == 0)
    {
        printf("NO\n");
        return 0;
    }

    n = 0;
    ll m;
    for(int i = 1; i <= 1000000; i += 2)
    {
        m = 1LL * k * i;
        m -= 1;
        m /= 2;
        if(m <= 1LL * i * (i - 1) / 2)
        {
            n = i;
            break;
        }
    }

    printf("YES\n");
    printf("%d %lld\n", 2 * n, 2 * m + 1);

    int cnt = 0;

    int pos = n - 1;
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            //dbg2(i, pos + 1);
            if(i == n && deg[i] >= k - 1)   break;
            if(i != n && deg[i] >= k)       break;
            pos = (pos + 1) % n;
            int v = pos + 1;
            if(G[i][v] || i == v)           continue;
            if(v != n && deg[v] >= k)       continue;
            if(v == n && deg[v] >= k - 1)   continue;
            printedge(i, v);
            ++cnt;
        }
    }
    printf("%d %d\n", n, 2 * n);
    return 0;
}

938D - Buy a Ticket (建图 + 最短路)

1. 题意

给你一个有 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \le n \le 2 \cdot 10^5) n(1n2105) 个点和 m ( 1 ≤ m ≤ 2 ⋅ 1 0 5 ) m(1 \le m \le 2 \cdot 10^5) m(1m2105) 条边的无向图

每个点 i i i 都有其权值 a i ( 1 ≤ a i ≤ 1 0 12 ) a_i(1 \le a_i \le 10^{12}) ai(1ai1012)

d ( i , j ) d(i,j) d(i,j) 表示点 i i i 到点 j j j 之间最短路径的长度。

对于每个 i ∈ [ 1 , n ] i \in [1,n] i[1,n],求
min ⁡ j = 1 n { 2 ⋅ d ( i , j ) + a j } \min_{j=1}^{n}\left\{2\cdot d(i,j) + a_j\right\} j=1minn{2d(i,j)+aj}

2. 解题思路

本题采用朴素做法,会 TLE。

对于 Input 中给定的边,按其边权二倍存边。

之后,开一个超级源点 0 0 0,向每个点 i i i 建边,权值 a i a_i ai

则点 i i i 的答案为超级源点 0 0 0 i ​ i​ i最短路径的长度。

时间复杂度: O ( ( 2 n + m ) log ⁡ n ) O((2n+m)\log n) O((2n+m)logn)

3. 代码

int main()
{
    int m, u, v;
    ll w;
    scanf("%d%d", &n, &m);
    init();

    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%lld", &u, &v, &w);
        addedge(u, v, 2 * w);
        addedge(v, u, 2 * w);
    }

    for(int i = 1; i <= n; i++)
    {
        scanf("%lld", &w);
        addedge(0, i, w);
        addedge(i, 0, w);
    }

    Dijkstra();

    for(int i = 1; i <= n; i++)
    {
        if(i > 1)   printf(" ");
        printf("%lld", d[i]);
    }
    return 0;
}

766D - Mahmoud and a Dictionary (建图 + 并查集 + LCA + 离线维护)

1. 题意

给你 n ( 2 ≤ n ≤ 1 0 5 ) n(2 \le n \le 10^5) n(2n105) 个单词。

单词之间存在两种关系 t t t

t = 1 t=1 t=1,表示两个单词为同义词;若 t = 2 t=2 t=2,表示两个单词为反义词

现在给出 m ( 1 ≤ m ≤ 1 0 5 ) m(1 \le m \le 10^5) m(1m105) 个关系和 q ( 1 ≤ q ≤ 1 0 5 ) q(1 \le q \le 10^5) q(1q105) 个查询。

给出 m m m 个关系时,若当前给出的两个单词不存在关系,则建立新的关系

存在关系,则输出已经存在的关系与给出的关系是否相一致

之后 q q q 个查询,对于每个查询,输出两个单词之间的关系或输出两个单词之间没有关系。

2. 解题思路

每个单词对应无向图中的一个点。

对于同义词关系,建立边权为 0 0 0 的无向边;

对于反义词关系,建立边权为 1 1 1 的无向边。

每两个点之间最多有一条边,且不存在,因此每个连通分量构成一棵

两个单词为反义词当且仅当二者距离为奇数

两个单词为同义词当且仅当二者距离为偶数

并查集维护两个单词之间是否存在关系,即两点是否在同一连通分量中。

因为两个单词之间的关系是唯一确定的,因此对于 m m m 个关系,若关系不存在,则按照上述方法添加关系,并输出 YES,否则需要先将关系存储起来,待所有关系确定之后,再去判断是否一致。

图建立完成之后,通过 DFS 计算每个点 i i i 与根节点的距离 d i d_i di,则两点 u , v u,v u,v 之间的距离为 d u + d v − 2 ⋅ d L C A ( u , v ) d_u+d_v-2\cdot d_{LCA(u,v)} du+dv2dLCA(u,v).

按照上述思路处理剩余的过程。

时间复杂度: O ( m α ( n ) + ( m + q ) log ⁡ n ) O(m\alpha(n)+(m+q)\log n) O(mα(n)+(m+q)logn)

3. 代码

int n, cnt, range;
int father[maxn], head[maxn], rank_[maxn], dp[maxn], depth[maxn], f[maxn][65];
struct edge
{
    int v, nxt;
    int w;
} Edge[2 * maxn];
void init()
{
    for(int i = 0; i <= n; i++)
    {
        head[i] = -1;
        father[i] = i;
    }
    cnt = 0;
    range = (int)log(n) / log(2) + 1;
}
void addedge(int u, int v, int w)
{
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
int find(int id)
{
    return father[id] == id ? id : father[id] = find(father[id]);
}
void merge(int a, int b)
{
    a = find(a);
    b = find(b);
    if(a != b)
    {
        if(rank_[a] > rank_[b]) father[b] = a;
        else
        {
            father[a] = b;
            if(rank_[a] == rank_[b])    rank_[b]++;
        }
    }
}
unordered_map<string, int> mp;
bool verdict[maxn];
pair<pii, int> query[maxn];
void dfs(int id, int fa)
{
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        int w = Edge[i].w;
        if(v == fa) continue;
        dp[v] = dp[id] + w;
        depth[v] = depth[id] + 1;
        f[v][0] = id;
        for(int j = 1; j <= range; j++) f[v][j] = f[f[v][j - 1]][j - 1];
        dfs(v, id);
    }
}
int lca(int a, int b)
{
    if(depth[a] < depth[b]) swap(a, b);
    for(int i = range; i >= 0; i--)
        if(depth[f[a][i]] >= depth[b])    a = f[a][i];
    if(a == b)  return a;
    for(int i = range; i >= 0; i--)
    {
        if(f[a][i] != f[b][i])
        {
            a = f[a][i];
            b = f[b][i];
        }
    }
    return f[a][0];
}
int main()
{
    int m, q, op;
    string str, stra, strb;
    scanf("%d%d%d", &n, &m, &q);
    init();
    for(int i = 1; i <= n; i++)
    {
        cin >> str;
        mp[str] = i;
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d", &op);
        cin >> stra >> strb;
        int a = mp[stra];
        int b = mp[strb];
        if(find(a) != find(b))
        {
            verdict[i] = true;
            addedge(a, b, op - 1);
            addedge(b, a, op - 1);
            merge(a, b);
        }
        query[i] = make_pair(pii(a, b), op - 1);
    }
    for(int i = 1; i <= n; i++)
    {
        if(!depth[i])
        {
            depth[i] = 1;
            dfs(i, -1);
        }
    }
    for(int i = 1; i <= m; i++)
    {
        if(verdict[i])  printf("YES\n");
        else
        {
            int a = query[i].first.first;
            int b = query[i].first.second;
            int c = query[i].second;
            int LCA = lca(a, b);
            int len = dp[a] + dp[b] - 2 * dp[LCA];
            len %= 2;
            if(len == c)    printf("YES\n");
            else printf("NO\n");
        }
    }
    for(int i = 1; i <= q; i++)
    {
        cin >> stra >> strb;
        int a = mp[stra];
        int b = mp[strb];
        if(find(a) != find(b))
        {
            printf("3\n");
            continue;
        }
        int LCA = lca(a, b);
        int len = dp[a] + dp[b] - 2 * dp[LCA];
        len %= 2;
        printf("%d\n", len + 1);
    }
    return 0;
}

388B - Fox and Minimal path (二进制 + 建图)

1. 题意

建立一个没有自环和重边,有 n ( 2 ≤ n ≤ 1 0 3 ) n(2 \le n \le 10^3) n(2n103) 个节点的无向图 G G G,使得从点 1 1 1 到点 2 2 2 的最短路径数量恰好为 k ( 1 ≤ k ≤ 1 0 9 ) k(1 \le k \le 10^9) k(1k109)

2. 解题思路

k k k 进行二进制分解,发现 k k k 有若干个 2 2 2 的幂次组成。

因此,可以考虑 2 2 2 的每个幂次的贡献进行建图。

k = 9 k=9 k=9 为例,则 k k k 的二进制分解为:

k = 1 ⋅ 2 3 + 0 ⋅ 2 2 + 0 ⋅ 2 1 + 1 ⋅ 2 0 k = 1 \cdot 2^3 + 0 \cdot 2^2 + 0 \cdot 2^1 + 1 \cdot 2^0 k=123+022+021+120

则我们可按如下图方式建图:

其中 13 13 13 2 2 2 的边在 2 0 2^0 20 的系数为 1 1 1 的情况下才连上。

k k k 为其他值的情况与上面同理。

3. 代码

bool G[maxn][maxn];

void addedge(int u, int v)
{
    G[u][v] = G[v][u] = true;
}

int bit[55];

int main()
{
    int k;
    scanf("%d", &k);

    //k--;
    int cnt = 0;
    while(k)
    {
        bit[cnt++] = k % 2;
        k /= 2;
    }

    if(!cnt)    addedge(1, 2);
    else
    {
        int num = 3, last = 1;
        for(int i = cnt - 1; i >= 1; i--)
        {
            addedge(last, num);
            if(bit[i])
            {
                addedge(num, num + 1);
                addedge(num, num + 2);
            }
            if(i >= 2)
            {
                addedge(num + 1, num + 4);
                addedge(num + 1, num + 5);
                addedge(num + 2, num + 4);
                addedge(num + 2, num + 5);
            }
            else
            {
                addedge(num + 1, 2);
                addedge(num + 2, 2);
            }
            last = num;
            num += 3;
        }
        addedge(last, num + 6);
        if(bit[0])  addedge(num + 6, 2);
    }

    printf("1000\n");
    for(int i = 1; i <= 1000; i++)
    {
        for(int j = 1; j <= 1000; j++)
        {
            if(G[i][j]) printf("Y");
            else printf("N");
        }
        printf("\n");
    }
    return 0;
}

400D - Dima and Bacteria (DFS + 传递闭包)

1. 题意

共有 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1n105) 个细菌,细菌的种类数为 k ( 1 ≤ k ≤ 500 ) k(1 \le k \le 500) k(1k500)

对于第 i i i 种细菌,共有 c i c_i ci 个,编号从 ( ∑ k = 1 i − 1 c k ) + 1 (\sum_{k=1}^{i-1}c_k)+1 (k=1i1ck)+1 ∑ k = 1 i c k \sum_{k=1}^ic_k k=1ick

题目保证 ∑ i = 1 k c i = n \sum_{i=1}^kc_i=n i=1kci=n

另外,细菌和细菌之间存在 m ( 0 ≤ m ≤ 1 0 5 ) m(0 \le m\le 10^5) m(0m105) 种双向转化关系,从 u i u_i ui 号细菌转化为 v i v_i vi 号细菌需要 x i x_i xi 的代价。

现在要求每一种细菌转化为其他类型细菌的最小代价,并且规定,若同种细菌互转的最小代价如果大于 0 ​ 0​ 0,即为不合法。

2. 解题思路

首先对每一种细菌进行 DFS,若遇到访问过的点或者遇到边权非 0 0 0 的边,则跳过。

之后,判断所有的细菌是否都被访问过,若不是,证明同种细菌之间转化的最小代价非零,直接输出 No

否则,枚举每个点的出边,更新类型一次转换的最小代价(存储为二维数组 G ​ G​ G)。

之后,利用 Floyd \text{Floyd} Floyd,对 G G G 进行传递闭包,即可得到最终答案。

需要注意的是,同种细菌之间的转化可以借助于其他种类的细菌,千万不要忘记这一点,否则会 Wrong Answer

时间复杂度: O ( n + m + k 3 ) ​ O(n+m+k^3)​ O(n+m+k3)

3. 代码

int n, m, k, cnt;
int c[505], type[maxn], head[maxn];
bool mark[maxn];
int vis[maxn];
int G[505][505];
struct edge
{
    int v, w, nxt;
} Edge[2 * maxn];
void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
    memset(G, 0x3f, sizeof(G));
}
void addedge(int u, int v, int w)
{
    Edge[cnt].v = v;
    Edge[cnt].w = w;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void dfs(int id, int ty)
{
    if(type[id] == ty)  mark[id] = true;
    vis[id] = ty;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(vis[v] == ty || Edge[i].w != 0)   continue;
        dfs(v, ty);
    }
}
int main()
{
    int u, v, w;
    scanf("%d%d%d", &n, &m, &k);
    init();
    for(int i = 1; i <= k; i++) scanf("%d", &c[i]);
    int flag = 0;
    for(int i = 1; i <= k; i++)
    {
        for(int j = 1; j <= c[i]; j++)  type[++flag] = i;
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        addedge(u, v, w);
        addedge(v, u, w);
    }
    int sum = 1;
    for(int i = 1; i <= k; i++)
    {
        dfs(sum, i);
        sum += c[i];
    }
    int cnt = 0;
    for(int i = 1; i <= n; i++) cnt += mark[i];
    if(cnt < n)
    {
        printf("No\n");
        return 0;
    }
    printf("Yes\n");
    for(int i = 1; i <= k; i++) G[i][i] = 0;
    for(int i = 1; i <= n; i++)
    {
        for(int j = head[i]; j != -1; j = Edge[j].nxt)
        {
            int v = Edge[j].v;
            int w = Edge[j].w;
            int ta = type[i];
            int tb = type[v];
            G[ta][tb] = min(G[ta][tb], w);
            G[tb][ta] = min(G[tb][ta], w);
        }
    }
    for(int l = 1; l <= k; l++)
    {
        for(int i = 1; i <= k; i++)
        {
            for(int j = 1; j <= k; j++)
            {
                G[i][j] = min(G[i][j], G[i][l] + G[l][j]);
            }
        }
    }
    for(int i = 1; i <= k; i++)
    {
        for(int j = 1; j <= k; j++)
        {
            if(j > 1)   printf(" ");
            printf("%d", G[i][j] == 0x3f3f3f3f ? -1 : G[i][j]);
        }
        printf("\n");
    }
    return 0;
}

533B - Work Group (DFS + 树上 dp)

1. 题意

现在有 n ( 1 ≤ n ≤ 2 ⋅ 1 0 5 ) n(1 \le n \le 2 \cdot 10^5) n(1n2105) 名雇员,其中 1 ​ 1​ 1 号雇员为主任,它们组成一棵有根无向树。

a a a 能够管理 b b b,即 b b b a a a 的手下,当且仅当 b ∈ s u b t r e e ( a ) b \in subtree(a) bsubtree(a)

每名雇员 i i i 都有其工作效率 a i ( 1 ≤ a i ≤ 1 0 5 ) a_i(1 \le a_i \le 10^5) ai(1ai105)

现在每名雇员都要选取偶数个手下,组成工作小组,最终整个公司组成一个大的工作小组。

问这个大的工作小组的最大效率之和

2. 解题思路

d p [ i d ] [ 0 ] dp[id][0] dp[id][0] 为在 s u b t r e e ( i d ) subtree(id) subtree(id) 中选取偶数个手下,所可以得到的最大的权值和。

d p [ i d ] [ 1 ] dp[id][1] dp[id][1] 为在 s u b t r e e ( i d ) ​ subtree(id)​ subtree(id) 中选取奇数个手下,所可以得到的最大的权值和。

状态转移方程见代码。

最终的答案 d p [ 1 ] [ 1 ] dp[1][1] dp[1][1]

时间复杂度: O ( n ) O(n) O(n)

3. 代码

int head[maxn], n, cnt;
ll a[maxn], dp[maxn][2];

struct edge
{
    int v, nxt;
} Edge[2 * maxn];

void init()
{
    for(int i = 0; i <= n; i++)
    {
        head[i] = -1;
    }
    cnt = 0;
}

void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void dfs(int id, int fa)
{
    if(head[id] == -1)
    {
        dp[id][0] = 0;
        dp[id][1] = a[id];
        return;
    }
    dp[id][0] = 0;
    dp[id][1] = -0x3f3f3f3f3f3f3f3f;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(v == fa) continue;
        dfs(v, id);
        ll temp0 = dp[id][0];
        ll temp1 = dp[id][1];
        dp[id][0] = max(temp0 + dp[v][0], temp1 + dp[v][1]);
        dp[id][1] = max(temp0 + dp[v][1], temp1 + dp[v][0]);
    }
    dp[id][1] = max(dp[id][1], dp[id][0] + a[id]);
}

int main()
{
   	int u, root;
    scanf("%d", &n);
    init();
    for(int v = 1; v <= n; v++)
    {
        scanf("%d%lld", &u, &a[v]);
        if(u == -1) root = v;
        else
        {
            addedge(u, v);
            addedge(v, u);
        }
    }
    dfs(root, -1);
    printf("%lld\n", dp[root][1]);
    return 0;
}

228E - The Road to Berland is Paved With Good Intentions (2-SAT)

1. 题意

现在有一个有 n ( 1 ≤ n ≤ 100 ) n(1 \le n \le 100) n(1n100) 个节点, m ( 1 ≤ m ≤ n ( n − 1 ) 2 ) m(1 \le m \le \frac{n(n-1)}{2}) m(1m2n(n1)) 条边的无向图。

每条边都有边权 c i ( c i ∈ { 0 , 1 } ) ​ c_i(c_i \in \left\{0,1 \right\})​ ci(ci{0,1})

现在可以进行若干次(可以为零)如下操作:选择一个点,将其所有出边的权值全部取反

问是否可以通过若干次操作,使得所有边的权值为 1 1 1

若可以,输出所要操作的节点编号。

题目保证两点之间最多有一条路径。

2. 解题思路

本题为 2-SAT \text{2-SAT} 2-SAT 问题。

对于每一条边 e d g e i edge_i edgei,若其权值为 1 1 1,则连接它的两点要么全部操作,要么全部不操作;

若其权值为 0 0 0,则连接它的两点需要且只能操作其中一个。

求解思路与 776D \text{776D} 776D 相同,这里不再赘述。

3. 代码

int head[maxn], n, m, cnt, tot, top, num;
int dfn[maxn], low[maxn], st[maxn], col[maxn], var[maxn];
bool instack[maxn];
struct edge
{
    int v, nxt;
} Edge[8 * maxn * maxn];
void init()
{
    for(int i = 0; i <= 2 * n; i++) head[i] = -1;
    cnt = tot = top = num = 0;
}
void addedge(int u, int v)
{
    Edge[cnt].v = v;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void tarjan(int id)
{
    dfn[id] = low[id] = ++tot;
    st[++top] = id;
    instack[id] = true;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[id] = min(low[id], low[v]);
        }
        else if(instack[v])
            low[id] = min(low[id], dfn[v]);
    }
    if(dfn[id] == low[id])
    {
        int v;
        num++;
        do
        {
            v = st[top--];
            instack[v] = false;
            col[v] = num;
        } while(v != id);
    }
}
vector<int> ans;
int main()
{
    int u, v, w;
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);
        if(w)
        {
            addedge(u + n, v + n);
            addedge(v, u);
            addedge(v + n, u + n);
            addedge(u, v);
        }
        else
        {
            addedge(u + n, v);
            addedge(v + n, u);
            addedge(u, v + n);
            addedge(v, u + n);
        }
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        if(!dfn[i]) tarjan(i);
    }
    bool isok = true;
    for(int i = 1; i <= n; i++)
    {
        if(col[i] == col[i + n])
        {
            isok = false;
            break;
        }
    }
    if(!isok)
    {
        printf("Impossible\n");
        return 0;
    }
    for(int i = 1; i <= n; i++)
    {
        var[i] = col[i] > col[i + n];
        if(var[i])  ans.pb(i);
    }
    printf("%d\n", (int)ans.size());
    for(int i = 0; i < ans.size(); i++)
    {
        if(i > 0)   printf(" ");
        printf("%d", ans[i]);
    }
    return 0;
}

542C - Idempotent functions (DFS + 数学)

1. 题意

定义函数 g : { 1 , 2 , . . . , n } → { 1 , 2 , . . . , n } g:\left\{ 1,2,...,n\right\} \rightarrow \left\{ 1,2,...,n\right\} g:{1,2,...,n}{1,2,...,n}幂等函数,当且仅当对于任意的 x ∈ { 1 , 2 , . . . , n } x \in \left\{ 1,2,...,n\right\} x{1,2,...,n},满足 g ( g ( x ) ) = g ( x ) g(g(x))=g(x) g(g(x))=g(x)

f ( 1 ) ( x ) = f ( x ) f^{(1)}(x)=f(x) f(1)(x)=f(x) f ( k ) ( x ) = f ( f ( k − 1 ) ( x ) ) f^{(k)}(x)=f(f^{(k-1)}(x)) f(k)(x)=f(f(k1)(x)),对于任意的 k > 1 k>1 k>1

现在给你每一个 f ( x ) f(x) f(x) 的值,求最小的 k k k,使得 f ( k ) ( x ) f^{(k)}(x) f(k)(x)幂等函数

数据范围: 1 ≤ n ≤ 200 ​ 1 \le n \le 200​ 1n200

2. 解题思路

对于每一个 x ∈ { 1 , 2 , . . . , n } x \in \left\{ 1,2,...,n\right\} x{1,2,...,n},建立有向边 < x , f ( x ) > ​ <x,f(x)>​ <x,f(x)>

对于每一个连通分量 G i G_i Gi,若答案存在,则其中必有环,其最小的 k i k_i ki 值满足以下条件:

( 1 ) (1) (1) 大于等于离环最远的点到环的最短距离;

( 2 ) ​ (2)​ (2) 是环长度 T i ​ T_i​ Ti倍数

设连通分量的数量为 m m m LCM = lcm ( T 1 , T 2 , . . . T m ) \text{LCM}=\text{lcm}(T_1, T_2,...T_m) LCM=lcm(T1,T2,...Tm)

则最终的答案为
k = max ⁡ 1 ≤ i ≤ m { ⌈ k i LCM ⌉ ⋅ LCM } k=\max_{1 \le i \le m} \left\{\lceil \frac{k_i}{\text{LCM}} \rceil \cdot \text{LCM} \right\} k=1immax{LCMkiLCM}

3. 代码

int n;
int dfn[maxn], nxt[maxn], indeg[maxn], vis[maxn];
ll ans[maxn];
ll gcd(ll a, ll b)
{
    return b == 0 ? a : gcd(b, a % b);
}
ll lcm(ll a, ll b)
{
    return a / gcd(a, b) * b;
}
pll dfs(int id, int depth, int col)
{
    dfn[id] = depth;
    vis[id] = col;
    int v = nxt[id];
    if(vis[v] == col)
    {
        ll T = 1LL * (dfn[id] - dfn[v] + 1);
        return make_pair(max(1LL * dfn[id] / T * T, T), T);
    }
    return dfs(v, depth + 1, col);
}
ll divv(ll a, ll b)
{
    if(a % b == 0)  return a / b;
    return a / b + 1;
}
int main()
{
    int v;
    scanf("%d", &n);
    for(int u = 1; u <= n; u++)
    {
        scanf("%d", &v);
        nxt[u] = v;
        indeg[v]++;
    }
    ll LCM = 1;
    int pos = 0;
    for(int i = 1; i <= n; i++)
    {
        if(!vis[i])
        {
            pii res = dfs(i, 0, i);
            LCM = lcm(LCM, res.Se);
            ans[++pos] = res.Fi;
        }
    }
    ll answer = 1;
    for(int i = 1; i <= pos; i++)
    {
        answer = max(answer, divv(ans[i], LCM) * LCM);
    }
    printf("%lld\n", answer);
    return 0;
}

883G - Orientation of Edges (DFS)

1. 题意

给你一个有 n ( 2 ≤ n ≤ 3 ⋅ 1 0 5 ) n(2 \le n \le 3 \cdot 10^5) n(2n3105) 个节点和 m ( 1 ≤ m ≤ 3 ⋅ 1 0 5 ) m(1 \le m \le 3 \cdot 10^5) m(1m3105) 条边的既有无向边,又有有向边的图。

现在需要你给出两套方案,将所有的无向边都进行定向,分别使从点 s s s 能够通达的点最多和最少。

2. 解题思路

对无向边和有向边进行标记,并将无向边的两个方向进行标记,以区分是否改变方向。

之后,进行两遍 DFS,分别求最多的点和最少的点。

第一遍 DFS,遇到无向边就走;第二遍 DFS,遇到无向边的话就跳过

在 DFS 的过程中,对每条无向边进行标记,即可得到答案。

时间复杂度: O ( n ) O(n) O(n)

3. 代码

int head[maxn], n, cnt;
bool vis[maxn], vis2[2 * maxn];
int ans[2 * maxn][2];
int tot[2];

struct edge
{
    int v, nxt;
    int type, parity;
} Edge[2 * maxn];

void init()
{
    for(int i = 0; i <= n; i++) head[i] = -1;
    cnt = 0;
}

void addedge(int u, int v, int type, int parity = 0)
{
    Edge[cnt].v = v;
    Edge[cnt].parity = parity;
    Edge[cnt].type = type;
    Edge[cnt].nxt = head[u];
    head[u] = cnt++;
}

void dfs(int id)
{
    vis[id] = true;
    tot[0]++;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(Edge[i].type == 2)
        {
            if(Edge[i].parity == 0)
            {
                if(vis2[i] || vis2[i + 1])  continue;
                vis2[i] = vis2[i + 1] = true;
                ans[i][0] = ans[i + 1][0] = Edge[i].parity;
            }
            else
            {
                if(vis2[i] || vis2[i - 1])  continue;
                vis2[i] = vis2[i - 1] = true;
                ans[i][0] = ans[i - 1][0] = Edge[i].parity;
            }
        }
        if(vis[v])  continue;
        dfs(v);
    }
}

void dfs2(int id)
{
    vis[id] = true;
    tot[1]++;
    for(int i = head[id]; i != -1; i = Edge[i].nxt)
    {
        int v = Edge[i].v;
        if(Edge[i].type == 2)
        {
            if(Edge[i].parity == 0)
            {
                if(vis2[i] || vis2[i + 1])  continue;
                vis2[i] = vis2[i + 1] = true;
                ans[i][1] = ans[i + 1][1] = 1 - Edge[i].parity;
            }
            else
            {
                if(vis2[i] || vis2[i - 1])  continue;
                vis2[i] = vis2[i - 1] = true;
                ans[i][1] = ans[i - 1][1] = 1 - Edge[i].parity;
            }
        }
        else if(!vis[v]) dfs2(v);
    }
}

int main()
{
    int m, s, u, v, type;
    scanf("%d%d%d", &n, &m, &s);
    init();

    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d%d", &type, &u, &v);
        if(type == 1)
        {
            addedge(u, v, 1);
        }
        else
        {
            addedge(u, v, 2, 0);
            addedge(v, u, 2, 1);
        }
    }

    dfs(s);
    memset(vis, false, sizeof(vis));
    memset(vis2, false, sizeof(vis2));
    dfs2(s);

    printf("%d\n", tot[0]);
    for(int i = 0; i < cnt; i++)
    {
        if(Edge[i].type == 1)   continue;
        if(ans[i][0] == 0)  printf("+");
        else printf("-");
        i++;
    }
    printf("\n");

    printf("%d\n", tot[1]);
    for(int i = 0; i < cnt; i++)
    {
        if(Edge[i].type == 1)   continue;
        if(ans[i][1] == 0)  printf("+");
        else printf("-");
        i++;
    }
    return 0;
}

164A - Variable, or There and Back Again (DFS)

1. 题意

给你一个 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \le n \le 10^5) n(1n105) 个点, m ( 1 ≤ m ≤ 1 0 5 ) m(1 \le m \le 10^5) m(1m105) 条边的有向图。每个点 i i i 都有一个权值 v i ( 0 ≤ v i ≤ 2 ) v_i(0 \le v_i \le 2) vi(0vi2)

定义一个点是完美的,当且仅当这个点在一条以权值为 1 1 1 的点开始的,中间不出现权值为 1 1 1 的点,且以权值为 2 2 2 的点结束的路径上。

对于每个点,判断其是否完美。

2. 解题思路

对整个图进行两遍 DFS,第一遍 DFS 遍历以权值为 1 1 1 的点开始,不经过权值为 1 1 1 的点的路径。

第二遍 DFS 遍历以权值为 2 2 2 的点开始,遍历到权值为 1 1 1 的点结束。

这样,两次 DFS 都经过的点,就是完美的点。

3. 代码

int n, f[maxn];
bool vis[maxn], vis2[maxn];
bool ans[maxn];

vector<int> G[maxn];
vector<int> G2[maxn];

void dfs(int id)
{
    vis[id] = true;
    for(auto v: G[id])
    {
        if(vis[v] == true || f[v] == 1)   continue;
        dfs(v);
    }
}

void dfs2(int id)
{
    vis2[id] = true;
    for(auto v: G2[id])
    {
        if(f[v] == 1)   vis2[v] = true;
        if(vis2[v] == true)   continue;
        vis2[v] = true;
        dfs2(v);
    }
}

int main()
{
    int m, u, v;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &f[i]);
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &u, &v);
        G[u].pb(v);
        G2[v].pb(u);
    }
    for(int i = 1; i <= n; i++)
    {
        if(f[i] == 1)   dfs(i);
    }
    for(int i = 1; i <= n; i++)
    {
        if(f[i] == 2)   dfs2(i);
    }
    for(int i = 1; i <= n; i++)
    {
        if(vis[i] && vis2[i])  printf("1\n");
        else printf("0\n");
    }
    return 0;
}

你可能感兴趣的:(Codeforces,图论)