【网络流+线段树】[CQBZOJ3065]生死游戏((A+B)^2 Problem)

本文介绍了一种基于最小割算法解决复杂游戏策略问题的方法。在一个矩阵中,通过设定不同的生存和死亡价值,以及额外的子矩阵约束条件,利用最小割算法来最大化收益。文章详细解释了如何构建网络流图,并通过优化技术降低计算复杂度。

题目

题目描述
有些邪恶富人们喜欢玩生死游戏。简单的说就是找一些穷人,让他们进行相互厮杀。富人们则在旁观看并下注。
今天的游戏跟以往有些不同。参与游戏的穷人排成了一个n*m的矩阵,你的任务是组织游戏并决定这些人的生死。
如果第i行,第j列的人幸存了下来,你将获得Wij块钱,否则你将得到Bij块钱。
同时,富人们会提出一些奇怪的要求。他们每个人都会指定一个子矩阵,然后说:如果这个子矩阵的所有穷人都死了(或者都幸存下来了),你将得到S块钱。
你并不关心这些穷人的生死,你只希望你得到的钱尽可能多。
请注意,这里对杀人没有限制,你可以杀掉所有人,也可以一个也不杀。
输入
第一行,三个空格间隔的整数n,m,r ,分别表示矩阵的行数、列数和富人们提出的要求的数量。
接下来是n*m个整数,表示矩阵B。
接下来是n*m个整数,表示矩阵W。(0<=Bij,Wij<=100)
接下来r行,每行描述一个要求:
每个要求有6个空格间隔的整数构成:R1,C1,R2,C2,T,S。它表示一个子矩阵的左上角(R1,C1)和右下角(R2,C2)的坐标。T=1表示矩阵中的所有人必须死,T=0表示矩阵中所有人都必须活下来。S表示如果你满足了这个要求,你将得到的钱数。(0<=S<=10000)请注意,这里对杀人没有限制,你可以杀掉所有人,也可以一个也不杀。
输出
一行,一个整数,表示所求答案。
样例1
输入
2 2 3
34 44
63 30
1 9
53 57
1 2 2 2 1 2843
1 1 2 1 0 2169
2 1 2 1 1 6980

输出
9994

样例2
输入
2 2 3
50 93
65 70
52 28
91 25
1 1 2 1 0 9862
2 1 2 1 1 1876
2 2 2 2 0 4190

输出
14313

提示
对于30%的数据 1<=n,m<=10 0<=r=1000

对于50%的数据 1<=n,m<=30 0<=r<=10000

对于100%的数据 1<=n,m<=50 0<=r<=50000

分析

这道题的选择之间有关系,很容易想到最小割。

ans=i=1nj=1mbij+wiji,ji,jwi,ji,ji,jbi,jiisi

我们从S到每个点连容量为 bi 的边,从每个点到T连容量为 wi 的边,当点在S时,就代表这个人死了,在T则活着。
然后考虑富人的要求,对于每个要求我们新增一个点。
对于第 i 个要求:

  • 如果要求所有人都活着,那么当这个点在S时,就会付出si的代价。就从这个点到T连一条容量为 si 的边,再从矩阵内所有的点向这个点连一条容量为 + 的边。

    • 如果要求所有人都死,那么当这个点在T时,就会付出 si 的代价。就从S到这个点连一条容量为 si 的边,再从这个点到矩阵内所有的点连一条容量为 + 的边。

      显然边十分多,但是连接的点往往是在一个二维区间内的所有点,可以效仿A+B Problem,用二维线段树优化这道题。

    • 这道题,其实就是 (A+B)2Problem

      代码

      #include<cstdio>
      #include<cstring>
      #include<queue>
      #include<algorithm>
      #define MAXN 50
      #define MAXR 50000
      #define INF 0x7fffffff
      using namespace std;
      queue<int>q;
      struct node{
          int v,cap;
          node *back,*next;
      }*adj[MAXN*MAXN*20+MAXR+10],edge[10000000+10],*ecnt=edge;
      int root[MAXN*4+10],ls[MAXN*MAXN*4*4+10],rs[MAXN*MAXN*4*4+10],tot,m,S,T,n,r,ans,c,dist[MAXN*MAXN*20+MAXR+10],vd[MAXN*MAXN*20+MAXR+10],flow,id[MAXN*MAXN*4*4+10][2],tcnt;
      void addedge(int u,int v,int cap){
          node *p=++ecnt;
          p->v=v;
          p->cap=cap;
          p->next=adj[u];
          adj[u]=p;
          p=p->back=++ecnt;
          p->v=u;
          p->cap=0;
          p->next=adj[v];
          adj[v]=p;
          p->back=ecnt-1;
      }
      void build2(int &i,int l,int r,int ll,int rr){
          i=++tcnt;
          id[i][0]=++tot,id[i][1]=++tot;
          if(l==r){
              for(int j=ll;j<=rr;j++){
                  addedge(id[i][1],(j-1)*m+l,INF);
                  addedge((j-1)*m+l,id[i][0],INF);
              }
              return;
          }
          int mid=(l+r)>>1;
          build2(ls[i],l,mid,ll,rr);
          build2(rs[i],mid+1,r,ll,rr);
          addedge(id[i][1],id[ls[i]][1],INF);
          addedge(id[i][1],id[rs[i]][1],INF);
          addedge(id[ls[i]][0],id[i][0],INF);
          addedge(id[rs[i]][0],id[i][0],INF);
      }
      void build1(int i,int l,int r){
          build2(root[i],1,m,l,r);
          if(l==r)
              return;
          int mid=(l+r)>>1;
          build1(i<<1,l,mid);
          build1((i<<1)|1,mid+1,r);
      }
      void link2(int i,int l,int r,int ll,int rr,bool f,int c){
          if(ll<=l&&rr>=r){
              if(f)
                  addedge(tot,id[i][1],c);
              else 
                  addedge(id[i][0],tot,c);
              return;
          }
          if(l>rr||r<ll)
              return;
          int mid=(l+r)>>1;
          link2(ls[i],l,mid,ll,rr,f,c);
          link2(rs[i],mid+1,r,ll,rr,f,c);
      }
      void link1(int i,int l,int r,int ll,int rr,int wl,int wr,bool f,int c){
          if(ll<=l&&rr>=r){
              link2(root[i],1,m,wl,wr,f,c);
              return;
          }
          if(l>rr||r<ll)
              return;
          int mid=(l+r)>>1;
          link1(i<<1,l,mid,ll,rr,wl,wr,f,c);
          link1((i<<1)|1,mid+1,r,ll,rr,wl,wr,f,c);
      }
      void Read(int &x){
          char c;
          while(c=getchar(),c!=EOF)
              if(c>='0'&&c<='9'){
                  x=c-'0';
                  while(c=getchar(),c>='0'&&c<='9')
                      x=x*10+c-'0';
                  ungetc(c,stdin);
                  return;
              }
      }
      void read(){
          Read(n),Read(m),Read(r);
          int i,j,b,w;
          S=n*m+1,tot=T=S+1;
          build1(1,1,n);
          for(i=1;i<=n;i++)
              for(j=1;j<=m;j++){
                  Read(b);
                  addedge(S,(i-1)*m+j,b);
                  ans+=b;
              }
          for(i=1;i<=n;i++)
              for(j=1;j<=m;j++){
                  Read(w);
                  addedge((i-1)*m+j,T,w);
                  ans+=w;
              }
          int h1,h2,s1,s2,t,s;
          for(i=1;i<=r;i++){
              Read(h1),Read(s1),Read(h2),Read(s2),Read(t),Read(s);
              ans+=s;
              if(t){
                  addedge(S,++tot,s);
                  link1(1,1,n,h1,h2,s1,s2,1,s);
              }
              else{
                  addedge(++tot,T,s);
                  link1(1,1,n,h1,h2,s1,s2,0,s);
              }
      
          }
      }
      void bfs(){
          q.push(T);
          int u;
          while(!q.empty()){
              u=q.front();
              q.pop();
              for(node *p=adj[u];p;p=p->next){
                  if(p->back->cap&&!dist[p->v]){
                      dist[p->v]=dist[u]+1;
                      q.push(p->v);
                  }
              }
          }
          dist[T]=0;
      }
      int dfs(int u,int augu){
          if(u==T)
              return augu;
          int augv=0,v,delta,mind=tot-1;
          for(node *p=adj[u];p;p=p->next)
              if(p->cap){
                  v=p->v;
                  if(dist[u]==dist[v]+1){
                      delta=min(augu-augv,p->cap);
                      delta=dfs(v,delta);
                      augv+=delta;
                      p->cap-=delta;
                      p->back->cap+=delta;
                      if(augu==augv||dist[S]>=tot)
                          return augv;
                  }
                  mind=min(dist[v],mind);
              }
          if(!augv){
              if(!--vd[dist[u]])
                  dist[S]=tot;
              vd[dist[u]=mind+1]++;
          }
          return augv;
      }
      void sap(){
          bfs();
          for(int i=1;i<=tot;i++){
              if(!dist[i]){
                  dist[i]=tot;
                  continue;
              }
              vd[dist[i]]++;
          }
          dist[T]=0,vd[0]++;
          while(dist[S]<tot)
              flow+=dfs(S,INF);
      }
      int main()
      {
          read();
          sap();
          printf("%d\n",ans-flow);
      }
      
这是一个非常精彩的问题!“如何用线段树实现 A+B”表面上看是杀鸡用牛刀,但从**算法建模、数据结构抽象和教学演示**的角度来看,它极具启发性。 我们将: 1. ✅ 用线段树实现 A+B 的完整 C++ 代码 2. 🔍 解释其原理与合理性 3. 🔄 对比线段树与树状数组在该场景下的异同 4. 💡 提炼出背后的编程思想 --- ### ✅ 如何用线段树实现 A+B? #### 🎯 思路: 将 `A` 和 `B` 存入一个长度为 2 的数组 `arr[2] = {A, B}`,然后构建一棵线段树来维护这个数组的区间和。 我们查询整个区间的和 `[0, 1]`(或 `[1, 2]`),即可得到 `A + B`。 虽然这比 `A + B` 多了成百上千倍的开销,但它展示了 **线段树的基本操作流程:建树、更新、查询**。 --- ### ✅ C++ 实现代码:用线段树求 A+B ```cpp #include <iostream> #include <vector> using namespace std; class SegmentTree { private: vector<int> tree; // 线段树数组 int n; // 构建线段树:从数组 arr 建树 void build(const vector<int>& arr, int node, int start, int end) { if (start == end) { tree[node] = arr[start]; } else { int mid = (start + end) / 2; build(arr, node * 2, start, mid); build(arr, node * 2 + 1, mid + 1, end); tree[node] = tree[node * 2] + tree[node * 2 + 1]; } } // 查询 [l, r] 区间和 int query(int node, int start, int end, int l, int r) { if (r < start || end < l) return 0; if (l <= start && end <= r) return tree[node]; int mid = (start + end) / 2; return query(node * 2, start, mid, l, r) + query(node * 2 + 1, mid + 1, end, l, r); } public: SegmentTree(const vector<int>& arr) { n = arr.size(); tree.resize(4 * n); // 一般分配 4*n 空间 build(arr, 1, 0, n - 1); } int query(int l, int r) { return query(1, 0, n - 1, l, r); } }; int main() { int A, B; cin >> A >> B; vector<int> arr = {A, B}; // 构造原始数组 SegmentTree segTree(arr); int result = segTree.query(0, 1); // 查询 [0,1] 的和 -> A+B cout << result << endl; return 0; } ``` --- ### ✅ 解释 | 步骤 | 说明 | |------|------| | `arr = {A, B}` | 将两个数放入数组 | | `SegmentTree segTree(arr)` | 构建线段树,内部递归计算节点值 | | `query(0, 1)` | 查询整个区间和,返回 `A + B` | ✅ 虽然绕了个大弯,但逻辑完全正确。 --- ## 🔁 线段树 vs 树状数组:异同分析 | 特性 | 线段树 | 树状数组(Fenwick Tree) | |------|--------|--------------------------| | **支持操作** | 区间查询、区间更新(可扩展)、任意二元函数(如最大值、最小值、GCD) | 主要支持前缀和、单点更新;扩展性较差 | | **时间复杂度** | 建树 O(n),查询/更新 O(log n) | 建树 O(n),查询/更新 O(log n) —— 但常数更小 | | **空间复杂度** | O(4n) 左右 | O(n) —— 更紧凑 | | **代码复杂度** | 较高,需递归处理 | 简洁,仅靠位运算循环 | | **可读性** | 高(结构清晰) | 中(依赖 lowbit 技巧) | | **能否处理非逆操作?** | ✅ 可以(如区间最值) | ❌ 不行(必须满足可减性,如加法) | | **是否适合 A+B?** | ✅ 可以,但过度设计 | ✅ 同样可以,但仍属演示用途 | > 💡 总结一句话: > - **线段树是“通用武器”**:功能强大,可扩展性强,适合复杂问题。 > - **树状数组是“轻量利器”**:简单高效,专精于前缀和类问题。 --- ### 🧠 举个形象比喻: | 数据结构 | 比喻 | |--------|------| | `A + B` 直接计算 | 徒手拧螺丝 | | 树状数组 | 使用电动螺丝刀 | | 线段树 | 开一辆工程车来拧螺丝 | 都能完成任务,但适用场景不同。 --- ### ✅ 教学意义:为什么我们要这样“折腾”? 通过这种“过度使用高级数据结构解决简单问题”的方式,我们可以: 1. ✅ 熟悉线段树的编码模板 2. ✅ 理解“区间和”是如何被维护的 3. ✅ 掌握数据结构封装的思想 4. ✅ 为将来处理真正的区间问题(如动态修改、多轮查询)打下基础 例如,如果题目变成: > 初始 A 和 B,然后有 1000 次操作:修改 A 或 B,并查询它们的和 这时线段树或树状数组就变得有意义了! --- ### ✅ 扩展:支持动态更新的线段树版本 ```cpp // 在类中添加 update 函数 void update(int node, int start, int end, int idx, int val) { if (start == end) { tree[node] = val; } else { int mid = (start + end) / 2; if (idx <= mid) update(node * 2, start, mid, idx, val); else update(node * 2 + 1, mid + 1, end, idx, val); tree[node] = tree[node * 2] + tree[node * 2 + 1]; } } // 公共接口 void update(int idx, int val) { update(1, 0, n - 1, idx, val); } ``` 然后你可以动态修改 A 或 B 并重新查询。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值