Codeforces 429E 2-SAT

传送门

Codeforces 429E Points and Segments

题解

先贴一个最开始写的,后来觉得有误的方法:欧拉回路。

对于这类黑白染色,且存在差值绝对值不超过 1 的问题,可以考虑欧拉回路求解。因为按照欧拉回路将无向图的边指定方向后,任意点的入度与出度相等;若图中存在度数为奇的点,这样的点只有偶数个,那么将其两两一组进行连边,求解欧拉回路后删边,此时,任意点的入度与出度差值的绝对值不超过 1。

考虑 X 轴上任意点,合法的染色方案需要满足,覆盖它的区间中,蓝色与红色的数量差不超过 1。将区间看作边,其染色看作对边的定向(向左或右),根据欧拉回路的性质,X 轴上任一点上,向左与向右的边数量相等。为了处理区间的边界情况(即保证区间拼接起来,可以看作是一个大区间),将其表示为左闭右开 [ l , r + 1 ) [l, r+1) [l,r+1),对于每一个区间 l → r + 1 l\rightarrow r+1 lr+1 连一条边。对于奇数度的点,按照 X 轴顺序两两一组连边;因为若需要删除的边出现覆盖,绝对差值可能大于 1。

建图后求解欧拉回路即可。总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),瓶颈在离散化上。

上述算法的问题在于,原始的区间是左闭右闭,而为了满足区间左闭右开的性质,将 [ l , r ] [l,r] [l,r] 表示为 [ l , r + 1 ) [l,r+1) [l,r+1)。此时 ( r , r + 1 ) (r,r+1) (r,r+1) 应该与 [ l , r ] [l,r] [l,r] 逻辑一致,即表示某一种区间的染色方案,但实际上 ( r , r + 1 ) (r,r+1) (r,r+1) 并没有区间覆盖。这就导致了在 ( r , r + 1 ) (r,r+1) (r,r+1) 中可能出现不满足条件的点。下面的样例,存在一种欧拉回路的构造,使之在 ( 3 , 4 ) (3,4) (3,4) 区间内的任一点都不满足条件。

7
4 6
4 5
1 3
2 3
1 1
1 5
1 6

故上述欧拉回路的做法,仅在下述约束下正确:原始区间为左闭右开;或只要求在整数点上满足条件。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
    int to, rev, id;
    bool used;
};

int main()
{
    ios::sync_with_stdio(0), cin.tie(0);
    int N;
    cin >> N;
    vector<pair<int, int>> seg(N);
    vector<int> xs;
    for (int i = 0; i < N; ++i)
    {
        cin >> seg[i].first >> seg[i].second;
        ++seg[i].second;
        xs.push_back(seg[i].first);
        xs.push_back(seg[i].second);
    }
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(), xs.end()), xs.end());
    for (auto &p : seg)
    {
        p.first = lower_bound(xs.begin(), xs.end(), p.first) - xs.begin();
        p.second = lower_bound(xs.begin(), xs.end(), p.second) - xs.begin();
    }

    int V = xs.size();
    vector<vector<edge>> G(V);
    vector<int> deg(V);
    for (int i = 0; i < N; ++i)
    {
        int u = seg[i].first, v = seg[i].second;
        G[u].push_back({v, (int)G[v].size(), i, 0});
        G[v].push_back({u, (int)G[u].size() - 1, i, 0});
        ++deg[u], ++deg[v];
    }
    vector<int> odd;
    for (int v = 0; v < V; ++v)
    {
        if (deg[v] & 1)
            odd.push_back(v);
    }
    assert(((int)odd.size() & 1) == 0);
    for (int i = 0; i < (int)odd.size(); i += 2)
    {
        int u = odd[i], v = odd[i + 1];
        G[u].push_back({v, (int)G[v].size(), -1, 0});
        G[v].push_back({u, (int)G[u].size() - 1, -1, 0});
    }
    vector<int> res(N);
    vector<int> iter(V);
    function<void(int)> euler_path = [&](int v)
    {
        int n = G[v].size();
        for (int &i = iter[v]; i < n; ++i)
        {
            while (i < n && G[v][i].used)
                ++i;
            if (i == n)
                break;
            auto &e = G[v][i];
            if (e.id != -1)
                res[e.id] = e.to > v ? 1 : -1;
            e.used = G[e.to][e.rev].used = 1;
            euler_path(e.to);
        }
    };

    for (int v = 0; v < V; ++v)
        if (iter[v] == 0)
            euler_path(v);

    for (int i = 0; i < N; ++i)
        cout << max(0, res[i]) << " \n"[i + 1 == N];
    return 0;
}

如下是个人认为的正解:2-SAT

为了避免歧义,题中的 [ l , r ] [l,r] [l,r] 区间,下面称之为线段。

类似于扫描线的思想,将线段拆为左右端点,在 X 轴上依次向右考虑。每遇到一个端点 x x x,区间所覆盖线段数量的奇偶性改变;对于任一偶数区间,两种颜色染色数量相等。观察发现,在奇数区间,左右端点同时为线段左(或右)端点时,两个端点代表的线段所染颜色不同;反之,染色相同。定义
x i 为真 ⇔ 线段 i 染为红色 x_i 为真 \Leftrightarrow 线段i染为红色 xi为真线段i染为红色 转化为 2-SAT 问题。

上述方法基于一个假设,各个端点坐标互不相同。若出现端点相同的坐标,考虑这样一个问题:对区间某个点 x x x 操作 n n n 次,操作只有 ± 1 \pm 1 ±1 两种,求最终 x x x 上的值绝对值不超过 1 1 1 的方案。显然之前对于奇数区间端点两两配对的方法也是可行的。

最后的问题还是关于原始线段左闭右闭的性质。对于左端点,是右连续的;对于右端点,是左连续的。即,坐标为 x x x 的左端点,改变的区间是 [ x , ⋯   ) [x,\cdots) [x,);而右端点是 ( x , ⋯   ) (x, \cdots) (x,)。故相同坐标的左端点应优先于右端点被处理。若原始的区间就是左闭右开,则不用考虑这个问题,直接按照坐标排序即可。

分解强连通分量进行求解,总时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)

#include <bits/stdc++.h>
using namespace std;
struct Node
{
    int x, d, id;
};

int main()
{
    ios::sync_with_stdio(0), cin.tie(0);
    int N;
    cin >> N;
    vector<Node> ns;
    for (int i = 0; i < N; ++i)
    {
        int l, r;
        cin >> l >> r;
        ns.push_back({l, 1, i});
        ns.push_back({r, -1, i});
    }
    sort(ns.begin(), ns.end(), [&](auto &a, auto &b)
         {
             if (a.x != b.x)
                 return a.x < b.x;
             return a.d > b.d;
         });

    int V = N * 2;
    vector<vector<int>> G(V), rG(V);
    auto add_edge = [&](int u, int v)
    { G[u].push_back(v), rG[v].push_back(u); };

    for (int i = 0; i < V; i += 2)
    {
        auto &a = ns[i], &b = ns[i + 1];
        if (a.d == b.d)
        {
            add_edge(a.id + N, b.id);
            add_edge(b.id + N, a.id);
            add_edge(a.id, b.id + N);
            add_edge(b.id, a.id + N);
        }
        else
        {
            add_edge(a.id, b.id);
            add_edge(b.id + N, a.id + N);
            add_edge(a.id + N, b.id + N);
            add_edge(b.id, a.id);
        }
    }

    vector<int> scc(V);
    auto find_scc = [&]()
    {
        vector<bool> used(V);
        vector<int> vs;
        function<void(int)> dfs = [&](int v)
        {
            used[v] = 1;
            for (int u : G[v])
                if (!used[u])
                    dfs(u);
            vs.push_back(v);
        };
        function<void(int, int)> rdfs = [&](int v, int k)
        {
            used[v] = 1;
            scc[v] = k;
            for (int u : rG[v])
                if (!used[u])
                    rdfs(u, k);
        };
        for (int v = 0; v < V; ++v)
            if (!used[v])
                dfs(v);
        int k = 0;
        fill(used.begin(), used.end(), 0);
        for (int i = (int)vs.size() - 1; i >= 0; --i)
            if (!used[vs[i]])
                rdfs(vs[i], k++);
    };

    find_scc();
    for (int i = 0; i < N; ++i)
        cout << (scc[i] > scc[i + N] ? 1 : 0) << " \n"[i + 1 == N];
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CodeForces - 616D是一个关于找到一个序列中最长的第k好子段的起始位置和结束位置的问题。给定一个长度为n的序列和一个整数k,需要找到一个子段,该子段中不超过k个不同的数字。题目要求输出这个序列最长的第k好子段的起始位置和终止位置。 解决这个问题的方法有两种。第一种方法是使用尺取算法,通过维护一个滑动窗口来记录\[l,r\]中不同数的个数。每次如果这个数小于k,就将r向右移动一位;如果已经大于k,则将l向右移动一位,直到个数不大于k。每次更新完r之后,判断r-l+1是否比已有答案更优来更新答案。这种方法的时间复杂度为O(n)。 第二种方法是使用枚举r和双指针的方法。通过维护一个最小的l,满足\[l,r\]最多只有k种数。使用一个map来判断数的种类。遍历序列,如果当前数字在map中不存在,则将种类数sum加一;如果sum大于k,则将l向右移动一位,直到sum不大于k。每次更新完r之后,判断i-l+1是否大于等于y-x+1来更新答案。这种方法的时间复杂度为O(n)。 以上是两种解决CodeForces - 616D问题的方法。具体的代码实现可以参考引用\[1\]和引用\[2\]中的代码。 #### 引用[.reference_title] - *1* [CodeForces 616 D. Longest k-Good Segment(尺取)](https://blog.csdn.net/V5ZSQ/article/details/50750827)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Codeforces616 D. Longest k-Good Segment(双指针+map)](https://blog.csdn.net/weixin_44178736/article/details/114328999)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值