ABC 选集i

这篇博客详细介绍了如何利用差分法解决ABC 155 F题,讨论了暴力、DP和贪心方法的局限性,并深入解析了差分法的思路和应用。博主通过分析串和操作的关系,提出将区间操作转化为单点操作,通过DFS解决,证明了DFS的正确性。此外,还提及了类似问题CH0304 IncDec Sequence的解法,以及POJ 3263 Tallest Cow题目的处理技巧,其中涉及对可视范围的处理和避免重复计算的方法。
摘要由CSDN通过智能技术生成
ABC 155 F Perils in Parallel

在这里插入图片描述
在这里插入图片描述
这个题可以抽象成一个串和若干个操作,每个操作能翻转串的子串,问这些操作能否把串翻转成全0
首先想到暴力思路,操作M能让串处于 S 1 S_1 S1 S 2 S_2 S2两种状态,问题就划分为操作 1 , 2 , . . . , M − 1 1,2,...,M-1 1,2,...,M1 是否能将 S 1 S_1 S1或者 S 2 S_2 S2的串翻转成全0,继续划分,分支总数为 2 M 2^M 2M,时间复杂度到达指数级肯定是不行的。
接着想想dp方法 d p [ M ] [ ? ] dp[M][?] dp[M][?]涉及到M个操作, ? ? ?取什么样的属性呢?想想 d p [ M ] [ ? ] dp[M][?] dp[M][?] d p [ M − 1 ] [ ? ] dp[M-1][?] dp[M1][?]中怎么提取信息? 用排除法的话,?不能是串的状态,因为串太长所以不能表示状态,?为空 dp[M]能得到的信息大概是dp[M-1]为true的时候 dp[M]为true,而dp[M-1]不为true的时候,dp[M]能否为true不能判断,感觉dp不行啊
接着想想贪心的方法,M个操作里面有偏序关系存在吗?要说操作之间的区别那就是操作之前存在影响位数大小的差别,差别不能决定偏序关系,而且M个操作不一定都要选,即使有偏序关系也选不了,所以感觉贪心走不远
两大神器都没用,想想是不是有什么特殊的性质,如果一个操作只能翻一个位,那么这个位肯定最后能翻成0,这一位相当没有,如果两个操作翻的位没有交集,那么可以divide and conquer,如果有交集感觉不是很好处理
想到这里,觉得没什么思路,找答案,找到第一名noshi91的答案

#include <algorithm>
#include <functional>
#include <iostream>
#include <map>
#include <set>
#include <utility>
#include <vector>

int main() {
  using P = std::pair<int, int>;

  int n, m;
  std::cin >> n >> m;

  /*
  問題を言い換えるための下処理をする
  */

  std::vector<int> a(n), c(n + 1, 0); // c: 隣接する b の差分
  std::vector<P> bombs(n);            // first: A, second: B
  for (P &e : bombs) {
    std::cin >> e.first >> e.second;
  }
  std::sort(bombs.begin(), bombs.end()); // 座標の昇順にする
  for (int i = 0; i < n; i += 1) {
    a[i] = bombs[i].first;
    c[i] ^= bombs[i].second;
    c[i + 1] ^= bombs[i].second;
  }
  // この処理で c[i] = b[i] xor b[i+1] になる

  std::map<P, int> edge_index;
  // 辺の両端の pair から辺の index を取得する辞書
  // first <= second になっている
  // 多重辺があると消えてしまうが、今回は特に困らない
  std::vector<std::vector<int>> graph(n + 1); // グラフ
  for (int i = 0; i < m; i += 1) {
    int l, r;
    std::cin >> l >> r;
    // l, r を座標圧縮後の値に直す
    l = std::lower_bound(a.begin(), a.end(), l) - a.begin();
    r = std::upper_bound(a.begin(), a.end(), r) - a.begin();
    edge_index[{l, r}] = i;
    graph[l].push_back(r);
    graph[r].push_back(l);
  }

  /*
  下処理終わり
  グラフから辺を選んで両端を反転させ、c を全て 0 にする問題に帰着された
  */

  std::set<int> ans; // 使う辺の index の集合

  // DFS を行い、辺の使用不使用を決定していく
  std::vector<bool> visited(n + 1, false);
  std::function<void(int)> dfs = [&](int v) -> void {
    visited[v] = true;
    for (int u : graph[v]) {
      if (visited[u])
        continue;
      dfs(u);
      if (c[u] == 1) {
        // uv 辺を使用することに決定
        // edge_index[{u, v}] だと u > v の時に正しくアクセスできないことに注意
        int id = edge_index[std::minmax(u, v)];
        ans.insert(id);
        c[u] ^= 1;
        c[v] ^= 1;
      }
    }
  };
  // グラフが連結とは限らないので、0 から DFS するだけだと足りない
  for (int i = 0; i < n + 1; i += 1) {
    if (!visited[i])
      dfs(i);
    // この時点で i は探索済
    // それでも c[i] が 0 に出来てないなら、不可能
    if (c[i] == 1) {
      std::cout << -1 << std::endl;
      return 0;
    }
  }

  std::cout << ans.size() << std::endl;
  for (int e : ans) {
    std::cout << e + 1 << " ";
  }

  return 0;
}

这个方法是差分,我在算法竞赛书上找到差分这一章0x03,差分这个工具的作用是把原序列上的"区间操作"转化为差分序列上的"单点操作"进行计算 代码里面数组c就是差分数组
b[i] = c[0] xor c[1] xor … xor c[i] 注释是有问题的 应该是c[i] = b[i] xor b[i-1] 且 c [ 0 ] = = b [ 0 ] , c [ n ] = = b [ n − 1 ] c[0] == b[0],c[n] == b[n-1] c[0]==b[0],c[n]==b[n1]
第二个for循环是把每个cord的坐标根据a的坐标换成数组a的下标l和r 其中 a [ l ] a [ l + 1 ] . . . a [ r − 1 ] a[l] a[l+1]...a[r-1] a[l]a[l+1]...a[r1]是为该cord能翻转的炸弹的坐标,b[l] … b[r-1]都被翻转了,那么

因为 b[l] = c[0] xor c[1] xor … xor c[l],将c[l] flip的话 即 c[l]^=1; 那么b[0]…b[l-1]都不受影响,b[l]…b[n-1]全部都flip,如果将c[r] flip的话 b[r]…b[n-1]都被flip了,所以当c[l]和c[r]同时flip的话,b[l]…b[r-1]都会被flip,而b[1]…b[l-1]和b[r]…b[n-1]都不会flip

将由cord i引起的(l,r)的翻转 存到map中 map[{l,r}] = i,将(l,r)看作是边 存入图graph邻接表中
然后后面进行dfs,要让所有的b翻为0,即需要将所有的c翻为0,这里有个边界 就是 c [ n ] c[n] c[n],因为r可能为n,根据上面说的处理方式 翻转区间 [ l , n ) [l,n) [l,n)时 c[n]会被flip,所以c[n]存在的意义就在于方便处理边界 即翻转 [ l , n ) [l,n) [l,n)如果b[0] b[1] … b[n-1]全为0 c[n]应该为0

最后一个核心的点: 为什么dfs是对的
我想了半天没想出来,感觉这个有点玄,首先解决的是这个dfs的工作原理:
我决定把sample input 按这个算法来演算一下:
sample input 1:
b[0]b[1]b[2] 为 101 对应的c[0]c[1]c[2]c[3] 为 1111
边1 为 [0,3) 边2 为 [0,1) 边3 为 [0,0) 边4 为 [1,2)
dfs(0)中 假如先遍历的是1,先dfs(1)
dfs(1)中只能遍历2,先dfs(2)
dfs(2)中所有点邻接点都visited的了,函数里面就只将visited[2] 设为 true 回到dfs(1)
回到dfs(1) 因为c[2]是1 所以将c[1] 和 c[2] (刚从dfs2返回的) 翻转 c[1] c[2] 为 0
此时1所有的邻接边都遍历完了,回到dfs(0)
因为c[1] 为0 所以不进行c[0] 和 c[1] 的翻转 遍历3 dfs(3)
dfs(3)中所有邻接点都visited的了,设置visited[3] 为 true 就返回dfs(0)
因为c[3]是1 所以将c[0] 和 c[3] 翻转 翻转后全部都visited了 最后在for中检查 c中所有值为0

经过这个演算 我发现dfs是如果周围有未访问的点且c值为1的 那么就翻转连接到这个点的边,对于dfs函数 首先是dfs周围未访问的所有点,这样一圈一圈扩散到边界点,最后回到起始的dfs点,这个操作就像水的波纹扩散后遇到边界反射后又收回。
接下来问题是这个dfs一定正确吗,因为dfs每个点,遍历其邻接点的顺序可能会由于邻接表中邻接点的顺序不同而不同。
点u 有邻接点v 和 w 如果先遍历v 之后是返回u后才遍历的w 即除去(u,v)和(u,w)两个边v和w是不连通的,那么很明显 遍历顺序为v w 和 w v没有区别 因为两者不连通,不会影响到分别的连通点

如果除去(u,v)和(u,w)两个边v和w是连通的,因为dfs(x)能保证 x周围所有的边都能变成0(以深度遍历形成的遍历树来看,双亲结点能然其所有子结点变为0,一直向上传递导致根节点以外的所有结点都为0),无论是先dfs(v)还是dfs(w) 出了u以外的全部点都能翻成0,而可能的区别是 翻完以后根节点(u)可能1个为1 一个为0,但是每次翻都是翻两个点 要么 1 1 变 0 0 要么 1 0 变 0 1 可知,无论怎么翻1的个数的奇偶性是不变的,所以先dfs(v)还是先dfs(w) 效果是一样的

有上述可知,遍历其邻接点的顺序的不同不影响dfs的结果,所以一种遍历dfs成功了 其它种遍历dfs一定成功,反之一种失败了 其它都失败,所以这个dfs一定正确

趁热打铁把算法竞赛上面的差分都搞定

CH0304 IncDec Sequence

在这里插入图片描述
同上题一样,构造出 c [ 1 ] , c [ 2 ] , . . . , c [ n ] c[1], c[2], ... ,c[n] c[1]c[2]...c[n] 使得 a [ i ] = c [ 1 ] + c [ 2 ] + . . . + c [ i ] a[i] = c[1] + c[2] + ... + c[i] a[i]=c[1]+c[2]+...+c[i],要最后数都一样 那么就是要使 c [ 2 ] , c [ 3 ] , . . . , c [ n ] c[2], c[3], ... ,c[n] c[2]c[3]...c[n] 全部为0 边界c[n+1]先默认为0吧
使 [ l , r ] [l,r] [l,r]全部加1 就是使 c [ l ] c[l] c[l]加1 c [ r + 1 ] c[r+1] c[r+1]减1,反之全部减1 就是使 c [ l ] c[l] c[l]减1 c [ r + 1 ] c[r+1] c[r+1]加1,那么可知一次操作能使一个数加1 能使一个数减1,明显 最少的操作 是先将 c [ 2 ] . . . c [ n ] c[2] ... c[n] c[2]...c[n]的所有正数和所有负数 依次抵消,抵消剩下的 可以用c[1] 或者 c[n+1]与其配对进行抵消,所以最少的次数为 |p - q| + min(p,q) 其中p为 c [ 2 ] , c [ 3 ] , . . . , c [ n ] c[2], c[3], ... ,c[n] c[2]c[3]...c[n] 中大于0的数的和 q为 c [ 2 ] , c [ 3 ] , . . . , c [ n ] c[2], c[3], ... ,c[n] c[2]c[3]...c[n] 中小于0的数的和,因为min(p,q)的操作是不涉及到c[1]的,涉及到c[1]的操作可能是0 , 1, 2 ,… ,|p - q|种(因为可能|p-q|操作里面可能涉及c[1] 也可能不涉及c[1]而是涉及c[n+1]) 代码:
max(p,q) == min(p,q) + |p - q|

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;

typedef long long ll;
const int N = 100004;
ll a[N],c[N];

int main()
{
    ll n,p = 0,q = 0;//p 是所有正数的绝对值的和,q 是所有负数绝对值的和
    scanf("%lld",&n);
    for(ll i=1;i<=n;i++){
        scanf("%lld",a+i);
        c[i] = a[i] - a[i-1];
        if(i > 1 && c[i] > 0) p += c[i];
        if(i > 1 && c[i] < 0) q -= c[i];
    }
    printf("%lld\n%lld\n",max(p,q),max(p,q) - min(p,q) + 1);
    return 0;
}
POJ 3263 Tallest Cow

在这里插入图片描述
说实话这个题如果不是放在这个差分系列里面,我看到这个题可能不会往差分那里想,因为他没有差分的明显标志即对一个区间进行操作
我对这个题 感觉最后题目要求的输出是所有牛的最大可能高度 有两种理解,一种是同时存在的最大高度,另一种理解分别使第i头牛达到最高可能高度,第i牛达到最大可能高度时整体高度可能会不一样
先按简单的理解来 即第一种

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;

const int N = 10004;
int ans[N];//默认一开始所有牛都是等高 且高度都是等于最大牛的高度

int main()
{
    int n,i,h,r,a,b;
    scanf("%d%d%d%d",&n,&i,&h,&r);
    for(int k=1;k<=r;k++){
        scanf("%d%d",&a,&b);//a能看到b 就把a和b之间的所有牛的高度降1
        ans[min(a,b)+1]--;
        ans[max(a,b)]++;
    }
    for(int k=1;k<=n;k++)
        ans[k] += ans[k-1];//将差分数组还原成相对高度数组
    for(int k=1;k<=n;k++)
        printf("%d\n",ans[k] + h - ans[i]);//每头牛的相对高度差就为 h - ans[i]
    return 0;
}

示例通过了 但是是wrong answer,看了一下书上的代码 貌似数据可能出现第三头牛能看到第五头牛,第五头牛看得到第三头牛,这样处理一次就够了,加个map来记录,记录之前的min(a,b),max(a,b)组合是否已经处理过了,改了以后就AC了
AC代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;

const int N = 10004;
int ans[N];
map<pair<int,int>,bool> m;
int main()
{
    int n,i,h,r,a,b;
    scanf("%d%d%d%d",&n,&i,&h,&r);
    for(int k=1;k<=r;k++){
        scanf("%d%d",&a,&b);
        int u = min(a,b),v = max(a,b);
        if(!m[make_pair(u,v)]){
            ans[u+1]--;
            ans[v]++;
            m[make_pair(u,v)] = 1;
        }
    }
    for(int k=1;k<=n;k++)
        ans[k] += ans[k-1];
    for(int k=1;k<=n;k++)
        printf("%d\n",ans[k] + h - ans[i]);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值