省选日记 Day-29 - Day-25

省选日记 Day 29 - Day 25

Day 29 Mar 05, 2022, Saturday

在火车上学了一点插头 DP.

经典问题是给一张有障碍的棋盘, 求有多少条不同的回路可以覆盖所有无障碍的格点.

Day 28 Mar 06, 2022, Sunday

发现插头 DP 貌似不是我想象中只维护插头那么简单, 还需要维护插头的连通性, 不维护连通性搞出来的东西应该是

Day 27 Mar 07, 2022, Monday

今天开始用 DEV 了.

用 DEV 完成了插头 DP.

然后发现了巨大 UB:

如果我们使用某些版本 DEV 自带的 GCC 4.9.2 开启 c++14 编译如下代码

#include <bits/stdc++.h>
using namespace std;
signed main() {
  int n(10), aia[n+1];
  cout << sizeof aia;
}

得到输出为 1. 显然这里正确的结果是 44.

目前除了 4.9.2 版本的编译器会出问题以外, 比它早的和比它晚的都没有发现出这个问题.

遇到这个情况, 建议直接改为 c++11 进行调试, 或者使用自己的编译器, 因为 NOI Linux 使用的编译器版本为 9.3.0.

原因很复杂, 互联网上有说法: sizeof 是编译器在执行, 所以在分配内存之前这个表达式就是一个常量了. 但是 aia 作为 VLA, 它在编译时是不知道有多大的, 因此出现了问题.

Day 26 Mar 08, 2022, Tuesday

多米诺骨牌

求一个有障碍的棋盘被 12 的骨牌覆盖的方案数. 要求每相邻两行或两列至少有一个骨牌横跨.

我们可以用状压 DP 容斥得到答案.

我们可以轻松求出无横阔要求的方案数, 所以我们可以先对每个子矩阵求这个答案. 我们枚举起始行 U 和列的上下界 L,R, 这个时候就可以进行一次 O((nU+1)(RL+1)2RL+1) 的状压 DP 求出, 每次 DP 一行都可以得到一个子矩阵的方案数. 如果固定 U, 通过主定理可以求出这个起始行固定时枚举所有列区间的复杂度为 O((nU+1)m2m), 因此求出所有子矩阵的方案数的复杂度就是 O(n2m2m).

这个状压 DP 是十分基础的, 只要记录轮廓线下方每一个位置是否已经覆盖作为状态即可. 具体见代码.

接下来容斥: 我们如果强制使得一些列之间没有骨牌横跨, 其余的无要求, 那么我们求的就是把这些列从分界处劈开, 分别对不同的部分求方案数, 然后乘起来得到结果.

[a,b,c,d] 表示从第 a 行到第 b 行, 从第 c 列到第 d 列的方案数. 如果强制断开的列分界线编号依次为 Cut1,Cut2,...,Cute. 特别地, 我们认为 Cut0=0, Cute+1=m. 那么从第 a 行到第 b 行的强制断开 Cut 中的分界线的方案数 F(a,b) 就是:

i=0e[a,b,Cuti+1,Cuti+1]

如果我们在这个基础上要求所有相邻的行都有骨牌横跨, 也很简单, 需要先求出前 1,2,3,...,n1 行的方案数. 记前 i 行的方案数为 fi, 枚举从上到下第一个没有骨牌横跨的行间界限, 用总方案数减去这些情况的方案数. 即可得到答案.

fi=F(1,i)j=1i1fjF(j+1,i)

求任意 F(a,b) 的过程是 O(m) 的, 所以求 fn 的时间是 O(n2m) 的.

现在我们已经可以求出强行断开任意列的分界线, 并且保证所有相邻行间都有骨牌横跨的方案数了. 接下来只要枚举 m1 个分界线的子集 S, 表示是否强制断开, 然后根据 |S| 的奇偶进行容斥即可求得答案. 子集数量是 O(2m), 因此加上预处理总复杂度为 O(n2m2m).

const unsigned long long Mod(19901013);
inline void Mn(unsigned& x){x -= (x >= Mod) ? Mod : 0;}
unsigned long long Ans(0);
unsigned Range[16][16][16][16];
unsigned m, n, N;
unsigned A, B, C, D, t;
unsigned Cnt(0);
char a[17][16];
inline void Calc(unsigned U, unsigned L, unsigned R) {
  unsigned Lim(R - L + 1), Con(1 << Lim);
  unsigned f[2][Con], *Cur(f[0]), *To(f[1]);
  memset(Cur, 0, Con << 2); 
  Cur[0] = 1;
  for (unsigned i(U); i <= n; ++i) {
    for (unsigned j(L), J(0); j <= R; ++j, ++J) {
      if(!a[i][j]) continue;
      memset(To, 0, Con << 2);
      for (unsigned k(0); k < Con; ++k) if(Cur[k]) {
        if((k >> J) & 1) To[k ^ (1 << J)] += Cur[k], Mn(To[k ^ (1 << J)]);
        else {
          if(a[i][j + 1] && (j < R) && (!((k >> J) & 3))) To[k ^ (2 << J)] += Cur[k], Mn(To[k ^ (2 << J)]);
          if(a[i + 1][j]) To[k ^ (1 << J)] += Cur[k], Mn(To[k ^ (1 << J)]);
          To[k] += Cur[k], Mn(To[k]);
        }
      }
      swap(Cur, To);
    }
    Range[U][i][L][R] = Cur[0];
  }
}
unsigned long long Belt(unsigned *Ls, unsigned Cn, unsigned L, unsigned R) {
  unsigned long long Rt(1);
  for (unsigned Lst(0), i(0); i < Cn; ++i) Rt = Rt * Range[L][R][Lst][Ls[i]] % Mod, Lst = Ls[i] + 1;
  return Rt;
}
signed main() {
  n = RD(), N = (1 << ((m = RD()) - 1));
  for (unsigned i(1); i <= n; ++i) scanf("%s", a[i]);
  for (unsigned i(0); i <= 16; ++i) for (unsigned j(0); j <= 15; ++j) a[i][j] = (a[i][j] == '.');
  for (unsigned i(1); i <= n; ++i) for (unsigned j(0); j < m; ++j) for (unsigned k(j); k < m; ++k) Calc(i, j, k);
  for (unsigned i(0); i < N; ++i) {
    unsigned long long Pref[16];
    unsigned Cut[m], CntC(0);
    for (unsigned j(0); j < m - 1; ++j) if((i >> j) & 1) Cut[CntC++] = j;
    Cut[CntC++] = m - 1;
    for (unsigned j(1); j <= n; ++j) {
      Pref[j] = Belt(Cut, CntC, 1, j);
      for (unsigned k(1); k < j; ++k)
        Pref[j] = (Pref[j] + (Mod - Pref[k]) * Belt(Cut, CntC, k + 1, j)) % Mod;
    }
    Ans += ((CntC & 1) ? Pref[n] : Mod - Pref[n]);
  }
  printf("%llu\n", Ans % Mod);
  return Wild_Donkey;
}

USACO21DEC Closest Cow Wins S

这个题是道绿题, 但是困了我好久, 从读题开始花了 2H 才过.

把每个草皮看成是一个开区间, 只要我们的牛落在区间内就可以占领这块草皮.

处理出这些区间之后, 我就开始考虑如何贪心, 没有发现敌对牛把局面分割的性质.

我们发现没有任何草皮的区间包含了敌对牛, 所以两头相邻敌对牛之间我们可以认为是一个独立的问题.

因为草皮区间不是顶到左边敌对牛, 就是顶到右边敌对牛, 所以我们只要在两头敌对牛之间最多放两头牛就可以占领这中间所有草皮. 所以两头敌对牛之间只有三种情况: 不放, 放一头, 放两头.

我们可以通过双指针枚举放一头牛所能占领的最靠右的草皮, 维护最靠左的草皮, 算出每个子问题中放一头牛所能获得的最大收益. 那么放第二头牛的收益便是这个子问题中草皮价值总和减去第一头牛的最大收益.

我们可以考虑把这些收益一起排序, 从后往前选对应数量的牛即可.

为什么这样是对的, 我们知道一个子问题中第二头牛的贡献被统计是依赖第一头牛被放置的, 我们如何保证第一头牛的最大价值一定不比第二头牛放置后的价值和第一头牛的最大价值差小呢? 反证法: 如果第二头牛新占领的草皮价值比第一头牛占领的多, 那么可以在放第一头牛的时候直接把第一头牛放在第二头牛的位置上, 获得更多价值, 这和第一头牛获得最大价值相悖. 因此一定有先放第一头, 再放第二头.

pair<unsigned, unsigned long long> Grass[200005];
unsigned long long V[400005], Ans(0);
unsigned Rg[200005][2];
unsigned Cow[200005], b[400005];
unsigned m, n, My;
unsigned A, B, C, D, t;
unsigned Cnt(0), Tmp(0);
signed main() {
  n = RD(), m = RD(), My = RD();
  for (unsigned i(1); i <= n; ++i) Grass[i].first = RD() + 1, Grass[i].second = RD();
  for (unsigned i(1); i <= m; ++i) Cow[i] = RD() + 1;
  sort(Grass + 1, Grass + n + 1), sort(Cow + 1, Cow + m + 1);
  Cow[0] = 0, Cow[++m] = 1000000002;
  for (unsigned i(0), j(1); j <= n; ++j) {
    while(Cow[i + 1] < Grass[j].first) ++i;
    V[i + m] += Grass[j].second, Grass[j].second += Grass[j - 1].second;
    unsigned Dis;
    if((i ^ (m - 1)) && ((!i) || (Cow[i + 1] - Grass[j].first < Grass[j].first - Cow[i]))) {
      Rg[j][1] = Cow[i + 1]; 
      Dis = Cow[i + 1] - Grass[j].first;
      Rg[j][0] = (Dis <= Grass[j].first) ? (Grass[j].first - Dis) : 0;
    }
    else {
      Rg[j][0] = Cow[i];
      Dis = Grass[j].first - Cow[i];
      Rg[j][1] = Grass[j].first + Dis;
    }
  }
  for (unsigned i(0), L(0), R(1); R <= n; ++R) {
    while (Grass[R].first > Cow[i + 1]) ++i;
    while (Rg[L + 1][1] <= Rg[R][0]) ++L;
    V[i] = max(V[i], Grass[R].second - Grass[L].second);
  }  
  for (unsigned i(0); i < m; ++i) V[i + m] -= V[i];
  m <<= 1, sort(V, V + m);
  unsigned Lim(((My >= m) ? 0 : (m - My)) + 1);
  for (unsigned i(m); i >= Lim; --i) Ans += V[i - 1];
  printf("%llu\n", Ans);
  return Wild_Donkey;
}

不要手贱写树套树!!!

我用线段树维护这些线段, 每个节点存一个 set 维护覆盖这个节点的线段.

每次查询全局最大值, 然后把覆盖这个单点的所有线段在树上抹去.

我这样的算法会在某些情况出问题, 如果有三条线段表示开区间:

(1,2), (1,7), (4,10), (9,10).

如果我一开始选择了线段 (1,7), (4,10), 之后需要再选两次才能选完这些线段. 总共需要选 3 次.

但是这 4 条线段只需要两次就可以全部选完, 所以这个做法是错误的.

遗憾的代码:

pair<unsigned, unsigned> Grass[200005];
unsigned Rg[200005][2], Cow[200005], b[400005];
unsigned Stack[200005], STop(0);
unsigned long long Ans(0);
unsigned m, n, My;
unsigned A, B, C, D, t;
unsigned Cnt(0), Tmp(0);
struct Node {
  set<unsigned> List;
  Node* LS, *RS;
  unsigned long long Mx, Tag;
  unsigned MxP;
}N[800005], *CntN(N);
inline void Build(Node*x, unsigned L, unsigned R) {
  x->MxP = L;
  if(L == R) return;
  unsigned Mid((L + R) >> 1);
  Build(x->LS = ++CntN, L, Mid);
  Build(x->RS = ++CntN, Mid + 1, R);
}
inline void Chg(Node* x, unsigned L, unsigned R) {
  if((A <= L) && (R <= B)) {
    x->List.insert(C), x->Tag += Grass[C].second, x->Mx += Grass[C].second;
    return;
  }
  unsigned Mid((L + R) >> 1);
  if(A <= Mid) Chg(x->LS, L, Mid);
  if(B > Mid) Chg(x->RS, Mid + 1, R);
  if(x->LS->Mx >= x->RS->Mx) x->Mx = x->Tag + x->LS->Mx, x->MxP = x->LS->MxP;
  else x->Mx = x->Tag + x->RS->Mx, x->MxP = x->RS->MxP;
}
inline void Access(Node* x, unsigned L, unsigned R) {
  for (auto i:x->List) Stack[++STop] = i;
  if(L == R) return;
  unsigned Mid((L + R) >> 1);
  if(A <= Mid) Access(x->LS, L, Mid);
  else Access(x->RS, Mid + 1, R);
}
inline void Ers(Node* x, unsigned L, unsigned R) {
  if((A <= L) && (R <= B)) {
    x->List.erase(C);
    x->Tag -= Grass[C].second, x->Mx -= Grass[C].second;
    return;
  }
  unsigned Mid((L + R) >> 1);
  if(A <= Mid) Ers(x->LS, L, Mid);
  if(B > Mid) Ers(x->RS, Mid + 1, R);
  if(x->LS->Mx >= x->RS->Mx) x->Mx = x->Tag + x->LS->Mx, x->MxP = x->LS->MxP;
  else x->Mx = x->Tag + x->RS->Mx, x->MxP = x->RS->MxP;
}
signed main() {
  n = RD(), m = RD(), My = RD();
  for (unsigned i(1); i <= n; ++i) Grass[i].first = RD() + 1, Grass[i].second = RD();
  for (unsigned i(1); i <= m; ++i) Cow[i] = RD() + 1;
  sort(Grass + 1, Grass + n + 1), sort(Cow + 1, Cow + m + 1);
  Cow[0] = 0, Cow[++m] = 1000000002;
  for (unsigned i(0), j(1); j <= n; ++j) {
    while(Cow[i + 1] < Grass[j].first) ++i;
    unsigned Dis;
    if((i ^ (m - 1)) && ((!i) || (Cow[i + 1] - Grass[j].first < Grass[j].first - Cow[i]))) {
      Rg[j][1] = Cow[i + 1]; 
      Dis = Cow[i + 1] - Grass[j].first;
      Rg[j][0] = (Dis <= Grass[j].first) ? (Grass[j].first - Dis + 1) : 1;
    }
    else {
      Rg[j][0] = Cow[i] + 1;
      Dis = Grass[j].first - Cow[i];
      Rg[j][1] = Grass[j].first + Dis;
    }
  }
  for (unsigned i(1); i <= n; ++i) b[++Cnt] = Rg[i][0], b[++Cnt] = Rg[i][1];
  sort(b + 1, b + Cnt + 1), Cnt = unique(b + 1, b + Cnt + 1) - b;
  printf("Cnt %u\n", Cnt);
  Build(N, 1, Cnt - 1);
  for (unsigned i(1); i <= n; ++i) {
    A = Rg[i][0] = lower_bound(b + 1, b + Cnt, Rg[i][0]) - b;
    B = Rg[i][1] = lower_bound(b + 1, b + Cnt, Rg[i][1]) - b;
    C = i, Chg(N, 1, Cnt - 1);
  }
  while (My && N->Mx) {
    Ans += N->Mx, ++D;
    A = N->MxP, Access(N, 1, Cnt - 1);
    for (unsigned i(1); i <= STop; ++i)
      C = Stack[i], A = Rg[C][0], B = Rg[C][1], Ers(N, 1, Cnt - 1);
    --My, STop = 0;
  }
  printf("%llu %u\n", Ans, D);
  return Wild_Donkey;
}

Day 25 Mar 09, 2022, Wednesday

SDOI2012 基站建设

一开始看到这个题有标签 线段树, 平衡树, CDQ 分治, 做完后也没有用到这些 (如果 std::set 不算平衡树的话), 一看题解发现所有人不是 CDQ, 就是李超树, 还有手写平衡树的.

我十分不解, 放着现成的 set 为什么不用啊.

riRi 为第 i 个基站的发射和接收半径.

如果我们要把基站 i 的信号传到基站 j, 那么有式子:

(ri+Rj)2=(riRj)2+(xixj)2ri2+Rj2+2riRj=ri2+Rj22riRj+(xixj)24riRj=(xixj)2Rj=(xixj)24riRj=|xixj|2ri

显然如果一个基站的信号是从右边的基站接收来的, 那么我们一定可以找到一个方案使得它的信号从左边接收而来且代价更小. 因为信号的源头在最左端, 所以把一个方案中, 在 x 右边的基站都不选, 让信号直接从激活的基站中, x 左边最近的基站传到 x, 这样安排代价一定会更小, 因为减少了激活一些基站的代价, 中途额外代价也减少了.

接下来我们设信号传到基站 i 的最小代价为 fi, 考虑 DP.

fi=Vi+minj=1i1(fj+xixj2rj)

然后就有以 fjxj2rj 为因变量, 以 12rj 为自变量, 以 xi 为斜率, 以 fiVi 为截距的函数.

fjxj2rj=xi2rj+fiVi

维护下凸壳进行斜率优化 DP 即可. 答案即为 maxxi+rimfi.

代码过程中我犯了很多错, 最弱智的莫过于忘记维护凸壳, 写完还嘲讽这个题怎么这么好写并且过了两个点得了 11 分. (没错就是没有删点只有加点)

struct Pnt {
  double X, Y;
  inline const char operator < (const Pnt& x) const {return X < x.X;}
  inline char KLeq (const Pnt x, const Pnt y) const {
    double Del1(x.X - X), Del2(y.X - X), Del3(x.Y - Y), Del4(y.Y - Y);
    return Del3 * Del2 <= Del4 * Del1;
  }
};
set<Pnt> S;
double A, B, C;
double Ans(1e18), Tmp(0), XTmp(0);
unsigned long long m; 
unsigned n;
unsigned D, t;
signed main() {
  n = RD(), m = RD();
  A = RD(), B = RD(), C = RD();
  XTmp = (-1) / (2 * sqrt(B));
  S.insert({XTmp, C + XTmp * A});
  for (unsigned i(2); i <= n; ++i) {
    A = RD(), B = RD(), C = RD(), Tmp = 0;
    set<Pnt>::iterator It(S.begin()), Pre, Suf, TmpI;
    while (((++It) != S.end()) && ((It->Y - S.begin()->Y) <= (It->X * A - S.begin()->X * A))) S.erase(S.begin());
    Tmp = S.begin()->Y - S.begin()->X * A + C;
    if(A + B >= m) Ans = min(Tmp, Ans);
    XTmp = (-1) / (2 * sqrt(B));
    TmpI = Pre = Suf = It = (S.insert({XTmp, Tmp + XTmp * A})).first, ++Suf;
    if(Pre != S.begin() && Suf != S.end()) {
      --Pre;
      if(Pre->KLeq(*(Suf), *(It))) {S.erase(It); continue;}
    }
    if(It != S.begin()) {
      Suf = It, Pre = --It;
      while (Pre != S.begin()) {
        --Pre;
        if(Pre->KLeq(*Suf, *It)) S.erase(It), It = Pre; else break;
      }
    }
    It = Pre = TmpI, ++It;
    if(It == S.end()) continue;
    Suf = It, ++Suf;
    while (Suf != S.end()) {
      if(Pre->KLeq(*Suf, *It)) S.erase(It), It = Suf, ++Suf; else break;
    }
  }
  printf("%.3lf\n", Ans);
  return Wild_Donkey;
}

ARC068F

一开始的转化和别的题解是一样的, 但是 DP 比较特别, 属于是把 O(n4) 的 DP 强行二维前缀和优化到 O(n2).

容易知道双端队列是单谷的. (想到了 NOIP2021 T3, 遗憾)

首先先考虑一种名为双重递减排列的东西, 这种排列可以被分成两个递减的子序列. 这个约束相当于序列的 LIS 长度不超过 2.

容易发现这种序列一定是合法的删除序列, 我们只要把两个递减的子序列一个正向一个反向组成一个单谷的序列作为双端队列, 然后按照我们想要的顺序取就可以得到目标序列.

我们还发现, 当 1 取出之后, 队列中如果还剩 x 个元素, 那么它们一定属于同一个递减子序列, 这是因为 1 一定是其中某个子序列的最后一个元素. 这时我们有 2x1 种取法, 也就是说无论先取小的还是先取大的都是合法的序列. 因此对于一个双重递减排列, 在 1 后面有 x 个元素, 那么它就对应着 2x1 种合法的删除序列. 特别地, 当 x=0 时, 这个序列对应 1 个合法删除序列.

本题要求 1 在第 K 次取出, 所以我们只要求 1 在第 K 位的双重递减排列的数量, 最后乘 2max(0,nK1) 即可.

对于一个排列, 它 LIS 的长度应该等于它逆排列的 LIS 长度, 因此我们不妨把问题转化为求 K 在第 1 位的双重递减排列的数量.

接下来考虑 DP 求出这种排列数量.

fi,j 表示长度为 i, 其中 j 在位置 1 的双重递减排列的数量.

边界情况: 1 在第 1 个位置, 这时第一个递减子序列只能是 1 本身, 另一个递减子序列为其它 i1 个数, 所以 fi,1=1.

接下来考虑一般情况, 我们假设第 1 个位置是 j, 那么 j 一定是第一个下降子序列的首个元素. 因此大于 j 的元素都是另一个下降子序列的元素.

假设最靠前的小于 j 的元素 a 在位置 nk+1, 那么 (1,nk] 区间内的 nk1 个数一定是 (k+1,n] 的连续递减的数.

因此后 k 位包含了 [1,k] 的数, 所以我们把后 k 位看成一个新的子问题. 也就是求第 1 位为 a, 长度为 k 的双重递减排列的数量.

k 的范围是 [j1,i), 因为有 j1 个比 j 小的数, 且 a 最早出现在第二位. 我们枚举 ka=l 进行转移. 状态 O(nm), 转移 O(nm), 总复杂度 O(n2m2), 因为 n, m 同阶, 所以认为复杂度为 O(n4).

fi,j=k=j1i1l=1j1fk,l

for (unsigned i(1); i <= n; ++i) {
  f[i][1] = 1;
  for (unsigned j(min(m, i)); j > 1; --j) {
    for (unsigned k(j - 1); k < i; ++k) {
      for (unsigned l(1); l < j; ++l) {
        f[i][j] += f[k][l], Mn(f[i][j]);
      }
    }
  }
}

O(n4), 26 个点过了 14 个. 用前缀和优化可以干到 O(n3).

for (unsigned i(1), l; i <= n; ++i) {
  f[i][1] = 1, l = min(m, i);
  for (unsigned j(2); j <= l; ++j) {
    f[i][j] = f[i][j - 1];
    for (unsigned k(j - 1); k < i; ++k) {
      f[i][j] += f[k][j - 1], Mn(f[i][j]);
    }
  }
}

优化后 26 个点过了 17 个. 不过我可以再次使用前缀和, 用二维前缀和把 DP 变成 O(n2). 然后顺利通过了此题.

const unsigned long long Mod(1000000007);
inline void Mn (unsigned& x) {x -= (x >= Mod) ? Mod : 0;}
unsigned f[2005][2005], m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Ans(0), Tmp(0);
signed main() {
  n = RD(), m = RD();
  for (unsigned i(1), l; i <= n; ++i) {
    f[i][1] = 1, l = min(m, i);
    for (unsigned j(2); j <= l; ++j)
      f[i][j] = f[i][j - 1] + f[i - 1][j - 1] - f[j - 2][j - 1] + Mod, Mn(f[i][j]), Mn(f[i][j]);
    for (unsigned j(1); j <= l; ++j) f[i][j] += f[i - 1][j], Mn(f[i][j]); 
  }
  Ans = (Mod + Mod + f[n][m] - f[n][m - 1] - f[n - 1][m] + f[n - 1][m - 1]) % Mod;
  for (unsigned i(m + 1); i < n; ++i) Ans <<= 1, Mn(Ans);
  printf("%u\n", Ans);
  return Wild_Donkey;
}
posted @   Wild_Donkey  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示