[SMOJ1832]交通违法

97 篇文章 0 订阅
11 篇文章 0 订阅

我们知道,如果一个无向连通图有 n 个结点和 n1 条边,那么它一定是一棵树。在此题里面,却有 n 条边。那么说明是在一棵树的基础上加了一条边。
这样一来,在图中,一定会有且只有一个环。

不难想到,最优的安排问题,往往是在 DP 的方向考虑。但是如果产生了环,肯定就有后效性。有后效性就不能做 DP。那怎么办呢?
很显然我们必须把环拿掉,至于删去环上的哪条边,其实都无所谓,因为删去后都能得到一棵正常的树。在新的树上就可以跑 DP 了。
先不管 DP 的问题,如何找出这个环?

对于多个环的情况,事后我也查阅了一些文章,但是这似乎是个 NP 问题…
参考:https://www.zhihu.com/question/32196067
http://blog.csdn.net/robin_xu_shuai/article/details/51898847

不过,本题当中只有一个环,那么就很好办了,可以从任意点开始跑一遍 dfs,如果搜到某一个已经被访问过的点,那么当前边为环上的边。当然,其实跑 bfs 也可以搞,而且还不用担心爆栈的问题,相对而言更加保险。反正我是用 bfs 做的。

现在得到这条多余的边之后,屏蔽它,得到一棵树。在这棵树上就可以做树形 DP。一开始我想成了“移动信号”,最后才发现:那题是照点,本题是照边。

其实每个点无非选或不选,不妨用 f[i][0] 表示以 i 为根的子树中,所有边都被照到,但 i 不放摄像头的最小值;类似地, f[i][1] 表示以 i 为根的子树中,所有边被照到,i 要放摄像头的最小值。在转移的时候,如果根结点 i 不选,则每个子结点都必须选,否则它们与 i 之间的连边就无法被照。如果 i 选,则子树选不选都可以。

但是别忘了,还有一条被我们屏蔽了的边!不妨称为 (u,v),为了把它照到, u v 必须至少要有一者装个摄像头。因此就分类讨论一下,加个限制,总共就跑两遍 dp,其实还是非常快的。

参考代码:

#include <algorithm>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int maxn = 1e5 + 100;

struct Edge { int from, to, next; } edge[maxn << 1];
int cntEdge;
int head[maxn];
void addEdge(int u, int v) {
    edge[++cntEdge].from = u;
    edge[cntEdge].to = v;
    edge[cntEdge].next = head[u];
    head[u] = cntEdge;
}

int n;
int a[maxn];

int bridge;
bool vis[maxn];
int father[maxn];
void bfs() {
    queue <int> q;
    while (!q.empty()) q.pop();
    q.push(1);
    vis[1] = true;
    while (!q.empty()) {
        int cur = q.front(); q.pop();
        for (int i = head[cur]; i; i = edge[i].next)
            if (!vis[edge[i].to]) {
                q.push(edge[i].to);
                vis[edge[i].to] = true;
                father[edge[i].to] = cur;
            } else if (edge[i].to != father[cur]) { bridge = i; return; }
    }
}

int dp[maxn][5];

void dfs1(int root, int pre) {
    dp[root][1] = 1;  
    for (int i = head[root]; i; i = edge[i].next) {  
        if (i == bridge || edge[i].to == edge[bridge].from) continue; //遍历的时候不能看多余边
        int child = edge[i].to;  
        if (child != pre) {  
            dfs1(child, root);  
            dp[root][0] += dp[child][1];

            dp[root][1] += min(dp[child][0], dp[child][1]);  
        }  
    }  
    if (root == edge[bridge].to) dp[root][0] = INT_MAX; //根不取,则多余边的另一点必须取
}  

void dfs2(int root, int pre) {  //无限制
    dp[root][1] = 1;  
    for (int i = head[root]; i; i = edge[i].next) {  
        if (i == bridge || edge[i].to == edge[bridge].from) continue;
        int child = edge[i].to;  
        if (child != pre) {  
            dfs2(child, root);  
            dp[root][0] += dp[child][1];

            dp[root][1] += min(dp[child][0], dp[child][1]);  
        }  
    }  
}  

int main(void) {
    freopen("1832.in", "r", stdin);
    freopen("1832.out", "w", stdout);
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        for (int j = 0; j < a[i]; j++) {
            int to;
            scanf("%d", &to);
            addEdge(i, to);
        }
    }
    bfs(); //求多余的边
    memset(dp, 0, sizeof dp);
    dfs1(edge[bridge].from, 0); //(新)根结点不取
    int ans = dp[edge[bridge].from][0];
    memset(dp, 0, sizeof dp);
    dfs2(edge[bridge].from, 0); //(新)根结点取
    printf("%d\n", min(ans, dp[edge[bridge].from][1]));
    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值