【做题笔记】板刷 CodeForces

CF1987D World is Mine#

第一想法是贪心的决策,考虑到是博弈论,每一轮决策肯定都是最优的。显然贪心做法假掉。

发现问题具有最优子结构与后效性,考虑 dp。

ai 数组排序,将相同元素打包成块,块长为 bai。设 fi,j 表示以第 i 个块结尾,剩余决策数为 j 的最优选择答案。

  • 若当前回合的剩余决策数立马用掉,不消除答案贡献,即为 fi,j=fi1,j+1

  • 若当前回合的剩余决策数留下,不消除答案贡献,即为 fi,j=fi1,j1+1

  • 若当前回合的剩余决策数用掉 bai 来消除答案贡献,即为 fi,j=fi1,j+bai

三种决策取最小即可。

由于博弈论的特殊性:双方在相同回合数时的总操作数相同,所以最后肯定不会剩余下决策数。所以答案为 fn,0

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1e17

using namespace std;

const int N = 5e3 + 10;

int T, n, a[N], b[N], f[N][N];

void solve() {
  int ans = 0, num = 0;
  cin >> n;
  For(i,1,n) b[i] = 0;
  For(i,0,n) For(j,0,n) f[i][j] = inf;
  For(i,1,n) cin >> a[i], b[a[i]]++;
  sort(a + 1, a + n + 1);
  int tot = unique(a + 1, a + n + 1) - a - 1;
  n = tot;
  f[0][0] = 0;
  For(i,1,n) {
    For(j,0,i) {
      if(j != 0) f[i][j] = min(f[i-1][j], f[i-1][j-1]) + 1;
      else f[i][0] = f[i-1][0] + 1;
      if(b[a[i]] + j <= i-1) f[i][j] = min(f[i][j], f[i-1][j + b[a[i]]]);
    }
  }
  cout << f[n][0] << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

时间复杂度 O(Tn2),记得多测清空。

CF1986D Mathematical Problem#

考虑搜索,O(T2n) 显然过不了。

先枚举相邻合并段,分析答案贡献的情况:先默认全加,发现答案上限为 19×9+99=261。然后将一部分序列改为乘法操作。

fi 表示前 i 个数组成的序列的最小答案,初始设为 [1,i] 的数值和。向前枚举 j,考虑将 [j+1,i] 这一段改为乘法操作,于是就有 f[i]=minj=0i1f[j]+k=j+1iak,显然后半部分可以预处理,于是就能做 O(n3) 的动态规划了。

因为乘法运算结果很大,所以当答案超过 261 时,就取 261 即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 262

using namespace std;

const int N = 25;

char a[N];

int T, n, num[N], sum[N], dp[N], ans = 261, fuck_you_code_bug_tot;

void solve() {
  cin >> n;
  int len = n-1, cnt0 = 0;
  ans = 261;
  For(i,1,n) cin >> a[i];
  For(i,1,n-1) {
    memset(num, 0, sizeof num);
    For(j,1,i-1) num[j] = a[j] - '0', sum[j] = sum[j-1] + num[j];
    num[i] = (a[i] - '0') * 10 + (a[i+1] - '0');
    sum[i] = sum[i-1] + num[i];
    For(j,i+2,n) num[j-1] = a[j] - '0', sum[j-1] = sum[j-2] + num[j-1];
    For(j,1,len) {
      cnt0 += (num[j] == 0);
      dp[j] = sum[j];
      int mul = num[j];
      FOR(k,j-1,0) {
        dp[j] = min(dp[j], dp[k] + mul);
        mul *= num[k];
        if(dp[j] > 261 || mul > 261) {
          break;
        } 
      }
    }
    ans = min({ans, dp[len], (cnt0 > 0ll ? 0ll : 261ll)});
  }
  cout << ans << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1983D Swap Dilemma#

可以观察到在操作数无限的情况下,大步换和两两换效果相同。可以考虑 a 数组最后两个数 [n1,n] 两两不停交换,b 数组同时冒泡到 a 数组的对应位置,然后看 [n,n1] 段的 a,b 数组是否相同。

发现 a,b 交换次数相同,然后就可以想到 a,b 两者逆序对奇偶性相同,则能有 a,b 相同的机会。可以这样想:a,b 数组排序后,a,b 数组一定相同。此时 a,b 数组的交换次数同奇同偶时,操作结果同步。

用树状数组算分别算逆序对比较奇偶性即可,时间复杂度 O(nlogn)

点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 2e5 + 10

using namespace std;

const int N = 4e5 + 10;

int T, n, a[N], b[N], p[N], t[N];

int lb(int x) {
  return x & -x;
}

int ask(int x) {
  int ans = 0;
  for (int i = x; i; i -= lb(i)) {
    ans += t[i];
  }
  return ans;
}

void upd(int x, int k) {
  for (int i = x; i <= inf; i += lb(i)) {
    t[i] += k;
  }
}

void solve() {
  vector<int> v1, v2;
  int ans1 = 0, ans2 = 0;
  cin >> n;
  For(i,1,n) cin >> a[i], p[i] = i, v1.push_back(a[i]);
  For(i,1,n) cin >> b[i], v2.push_back(b[i]);
  sort(v1.begin(), v1.end());
  sort(v2.begin(), v2.end());
  for (int i = 0; i < v1.size(); ++i) {
    if(v1[i] != v2[i]) {
      puts("NO");
      return ;
    }
  }
  For(i,1,n) {
    ans1 += ask(a[i]);
    upd(a[i], 1); 
  }
  For(i,1,n) upd(a[i], -1);
  For(i,1,n) {
    ans2 += ask(b[i]);
    upd(b[i], 1);
  }
  For(i,1,n) upd(b[i], -1);
  if((ans1 & 1) == (ans2 & 1)) {
    puts("YES");
  } else {
    puts("NO");
  }
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1986E Beautiful Array#

考虑假设两数 x,y 能在加 k 的操作后相同,则 xy0(modk),可以推得 xy(modk)。可以想到按 aimodk 排序,同时比较 ai 的值,保证 ai 升序。

定义“一组”的概念为 aimodk 相同的 ai 组成的升序序列。

对于一组来说,相邻两项凑一起算贡献肯定最优,自然推出了组长为偶数的情况,便是两两配对贡献。

考虑奇数组的情况:先枚举中间点 amid,此数对答案无贡献,然后从左至右两两配对贡献。这里因为要枚举中间点,所以暴力的时间复杂度为 O(n2)。可以考虑答案连续的贡献情况,每次抵消上一次的贡献,并将本次的贡献加上,每次取最小即可。(注意答案贡献的正负情况,自行推理)。

n 为偶数和奇数的情况来做:

  • n 为偶数时,若出现奇数组则无解,否则分组算贡献累加即可。

  • n 为奇数时,若出现两组以上的奇数组则无解,否则分组算贡献累加,奇数组单独处理。

总时间复杂度:O(nlogn)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1e11

using namespace std;

const int N = 2e5 + 10;

int T, n, k, a[N];

void solve() {
  int ans = 0;
  cin >> n >> k;
  For(i,1,n) cin >> a[i];
  sort(a + 1, a + n + 1, [](int x, int y){return (x % k == y % k ? x < y : x % k < y % k);});
  // For(i,1,n) cerr << a[i] << ' ';
  // cerr << '\n';
  if(n & 1) {
    int r = 1, l = 1, sum = 0, f = 0, Min = inf;
    vector<int> v;
    int cnt = 1;
    For(i,1,n) {
      if(a[i] % k == a[i+1] % k && i < n) cnt++, v.push_back(a[i]);
      else {
        if(cnt & 1) {
          v.push_back(a[i]);
          f++; r = i, l = r - v.size() + 1;
        } 
        else vector<int>().swap(v);
        cnt = 1;
      }
    }
    for (int i = 1; i <= l-1; i += 2) ans += a[i+1] - a[i];
    for (int i = r+1; i <= n; i += 2) ans += a[i+1] - a[i];
    For(i,l,r) sum += (((i-l+1) & 1) ? 1 : -1) * a[i];
    Min = min(Min, sum - a[l]);
    For(i,l+1,r) {
      if(!((i-l+1) & 1)) {
        sum -= a[i-1] * 2;
        sum += a[i] * 2;
      }
      Min = min(Min, sum - a[i]);
    }
    if(f > 1) {
      cout << "-1\n";
      return ;
    }
    cout << (ans + Min) / k << '\n';
  } else {
    int cnt = 1;
    For(i,1,n) {
      if(a[i] % k == a[i+1] % k && i < n) {
        cnt++;
      } 
      else {
        if(cnt & 1) {
          cout << "-1\n";
          return ;
        }
        cnt = 1;
      }
    }
    for (int i = 1; i <= n; i += 2) {
      ans += a[i+1] - a[i];
    }
    cout << ans / k << '\n';
  }
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1986F Non-academic Problem#

考虑快速计算贡献:设将图分为大小为 n1,n2 的两部分,显然有 n1+n2=n。则当前方案贡献为 min(n12n12,n22n22)

由于要最小化路径树,且只能删去一条边。所以最好的情况是将图尽可能分为两部分,所以满足删除条件的边就是割边。

先缩边双联通分量,然后枚举割边(缩成树后跑 dfs)。然后计算删去该割边的贡献即可,最后取最小的贡献作为答案。

总时间复杂度 O(n)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1e17

using namespace std;

const int N = 2e5 + 10;

struct Node {
  int v, nx, u;
} e[N], E[N]; 

int T, n, m, h[N], H[N], dfn[N], low[N], cut[N], dcc[N], Col, tot = 1, idx, Idx = 1, siz[N];

void add(int u, int v) {
  e[++tot] = (Node){v, h[u], u};
  h[u] = tot;
}

void Add(int u, int v) {
  E[++Idx] = (Node){v, H[u], u};
  H[u] = Idx;
}

void tarjan(int x, int fa) {
  dfn[x] = low[x] = ++idx;
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(!dfn[y]) {
      tarjan(y, x);
      low[x] = min(low[x], low[y]);
      if(low[y] > dfn[x]) cut[i] = cut[i ^ 1] = 1;
    } else if(y != fa) {
      low[x] = min(low[x], dfn[y]);
    }
  }
}

void dfs(int x, int cnt) {
  dcc[x] = cnt;
  siz[cnt]++;
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(dcc[y] || cut[i]) continue;
    dfs(y, cnt);
  }
}

void Dfs(int x, int fa) {
  for (int i = H[x]; i; i = E[i].nx) {
    int y = E[i].v;
    if(y == fa) continue;
    Dfs(y, x);
    siz[x] += siz[y];
  }
}

void solve() {
  int ans = inf;
  Col = idx = 0;
  Idx = tot = 1;
  memset(cut, 0, sizeof cut);
  For(i,1,n) {
    h[i] = H[i] = 0;
    dfn[i] = low[i] = dcc[i] = siz[i] = 0;
  }
  cin >> n >> m;
  For(i,1,m) {
    int u, v; cin >> u >> v;
    add(u, v), add(v, u);
  }
  tarjan(1, 1);
  For(i,1,n) {
    if(!dcc[i]) dfs(i, ++Col);
  }
  For(i,1,n) {
    for (int j = h[i]; j; j = e[j].nx) {
      int y = e[j].v;
      if(dcc[i] != dcc[y]) Add(dcc[i], dcc[y]);
    } 
  }
  Dfs(1, 0);
  For(i,1,n) {
    ans = min(ans, (n * n - n - (2 * siz[i] * (n - siz[i]))) / 2);
  }
  cout << ans << endl;
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1982D Beauty of the mountains#

单独考虑每一个山的贡献很难做,不妨以 k×k 的矩形为基础计算贡献。

我们发现每在 k×k 的矩阵内加上 x 的贡献,钦定有雪与无雪的山分别作正、负贡献,记为 bi。操作前的贡献为 sum,有雪的山总高度和无雪的格子的总高度相等其等价于判断 xibi+sum=0,即为 xibi=sum

这就是一个 n 阶裴蜀定理,若 sumgcdxi 的倍数,则 xi 有整数解。

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 505;

int T, n, m, k, a[N][N], s[N][N], Ans;

bool b[N][N];

int ask(int x1, int y1, int x2, int y2) {
  return s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
}

void solve() {
  Ans = 0;
  int sum = 0;
  cin >> n >> m >> k;
  memset(s, 0, sizeof s);
  For(i,1,n) For(j,1,m) cin >> a[i][j];
  For(i,1,n) {
    For(j,1,m) {
      char c; cin >> c;
      b[i][j] = c - '0';
    }
  } 
  For(i,1,n) {
    For(j,1,m) {
      sum += (b[i][j] ? a[i][j] : -a[i][j]);
      int F = (b[i][j] == 1 ? 1 : -1);
      s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + F;
    }
  }
  For(i,1,n-k+1) {
    For(j,1,m-k+1) {
      int k1 = ask(i, j, i+k-1, j+k-1);
      Ans = __gcd(Ans, k1);
    }
  }
  if(sum == 0) {
    puts("YES");
    return ;
  }
  if(Ans == 0) {
    puts("NO");
    return ;
  }
  if(sum % Ans == 0) puts("YES");
  else puts("NO");
  return ;
}

signed main() {
  // ios::sync_with_stdio(0);
  // cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1974D Ingenuity-2#

相同的操作下,操作顺序对最终的位置无影响。

机器人的位置初始都为 (0,0),所以只要操作同步即可。

此外注意到 NSEW 发生在同一个机器人上时,可以起到抵消的作用。

我们不希望只有一个机器人操作,所以让 R 机器人进行一次抵消操作,后面的抵消操作全部分配给 H。

其他的操作平均、同等分配即可。若无法分则判断无解。

点击查看代码
#include<bits/stdc++.h>
#define ll long long 
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 2e5 + 10, M = 200;

int T, n, b[M];

char a[N], ans[N];

void solve() {
  memset(b, 0, sizeof b);
  memset(ans, 0, sizeof ans);
  cin >> n >> (a + 1);
  if(n & 1) {
    cout << "NO\n";
    return ;
  }
  For(i,1,n) b[a[i]]++;
  int k1 = min(b['N'], b['S']), k2 = min(b['E'], b['W']);
  b['N'] -= k1, b['S'] -= k1, b['E'] -= k2, b['W'] -= k2;
  int K1 = k1;
  bool f1, f2; f1 = f2 = 0;
  for (int i = 1; i <= n && (k1 >= 0 || K1 >= 0); ++i) {
    if(a[i] == 'N' && k1 > 0) {
      if(f1) ans[i] = 'H';
      else if(!f1) ans[i] = 'R', f1 = 1;
      k1--; 
    }
    if(a[i] == 'S' && K1 > 0) {
      if(f2) ans[i] = 'H';
      else if(!f2) ans[i] = 'R', f2 = 1;
      K1--; 
    }
  }
  K1 = k2;
  for (int i = 1; i <= n && (k2 >= 0 || K1 >= 0); ++i) {
    if(a[i] == 'E' && k2 > 0) {
      if(f1) ans[i] = 'H';
      else if(!f1) ans[i] = 'R', f1 = 1;
      k2--; 
    }
    if(a[i] == 'W' && K1 > 0) {
      if(f2) ans[i] = 'H';
      else if(!f2) ans[i] = 'R', f2 = 1;
      K1--; 
    }
  }
  bool f = 1;
  for (int i : {'N', 'S', 'E', 'W'}) {
    f &= (b[i] == 0 && n <= 2);
    if(b[i] & 1) {
      cout << "NO" << '\n';
      return ;
    }
  }
  if(f) {
    cout << "NO" << '\n';
    return ;
  }
  For(i,1,n) {
    if((b[a[i]] & 1) && !ans[i]) ans[i] = 'R';
    else if(!(b[a[i]] & 1) && !ans[i]) ans[i] = 'H';
    b[a[i]]--;
  }
  For(i,1,n) cout << ans[i];
  cout << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1988D The Omnipotent Monster Killer#

第一眼有点像最大独立集,可以考虑往这方面想。

由于有了回合的限制,所以在 dp 时要多加一维来除去回合的后效性。设 dpx,y 表示第 x 个点在 y 回合被选中带来的最小贡献。很显然初始状态 dpx,y=ax×y

转移也很显然,父亲和儿子不能在同一回合被选中,所以只需要在 dfs 中枚举所有回合,然后把所有不同回合的儿子状态取最小贡献,转移至父亲即可。

考虑回合数,因为每一次选点会将树拆成森林,然后每一个小树又会减半,继续分。所以可以证明回合数在 logn 量级。

所以转移与计算贡献加上总时间复杂度 O(nlog2n),可以通过此题。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1e18

using namespace std;

const int N = 3e5 + 10;

struct Node {
  int v, nx;
} e[N << 1];

int T, n, a[N], h[N], dp[N][25], tot, m, sum, ans = inf;

void add(int u, int v) {
  e[++tot] = (Node){v, h[u]};
  h[u] = tot;
}

void dfs(int x, int fa) {
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == fa) continue;
    dfs(y, x);
  }
  if(x == 1) return ;
  For(i,1,m) {
    int num = inf;
    For(j,1,m) {
      if(i == j) continue;
      num = min(num, dp[x][j]);
    }
    dp[fa][i] += (num == inf ? 0 : num); 
  }
}

void solve() {
  ans = inf; sum = tot = 0;
  For(i,1,n) h[i] = 0;
  cin >> n;
  For(i,1,n) cin >> a[i], sum += a[i];
  For(i,1,n-1) {
    int u, v;
    cin >> u >> v;
    add(u, v), add(v, u);
  }
  m = (int)ceil(log2(n))+1;
  For(i,1,n) For(j,1,m) dp[i][j] = a[i] * j;
  dfs(1, 0);
  For(i,1,m) ans = min(ans, dp[1][i]);
  cout << ans << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1980D GCD-sequence#

考虑在原数列中删去一个数 aib 数列会怎样改变。

因为 bi=gcd(ai,ai+1),所以当 ai 删除时,会影响到 bi,bi1

先预处理出 l,r 表示从左到右、从右到左递增序列的区间,即 [1,l],[r,n] 为递增区间。

然后暴力枚举删除哪个 ai,然后将 ai 影响到的 bi,bi1 删去,插入新贡献 c=gcd(ai1,ai+1)。最后检查新贡献 c 在数列中是否能让数列满足递增,即修改的区间 [i2,i+1] 能否拼接上 [1,l],[r,n],并且 bi2cbi+1

总时间复杂度 O(n)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define inf 1e8

using namespace std;

const int N = 2e5 + 10;

int T, n, a[N], b[N];

void solve() {
  cin >> n;
  For(i,1,n) cin >> a[i];
  a[n+1] = a[0] = 0;
  For(i,1,n-1) b[i] = __gcd(a[i], a[i+1]);
  b[n] = inf;
  int l = 1, r = n-1;
  while(l <= n-1 && b[l+1] >= b[l]) l++;
  while(r >= 1 && b[r-1] <= b[r]) r--;
  if(l == n-2 || r == 2) {
    puts("YES"); return ;
  }
  For(i,1,n) {
    int New = __gcd(a[i-1], a[i+1]);
    if(New >= b[max(0ll, i-2)] && New <= b[min(n, i+1)] && l >= i - 2 && r <= i + 1) {
      puts("YES"); return ;
    }
  }
  puts("NO");
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1401F Reverse and Swap#

操作 1,4 是很常规的单点修改,区间查询操作,重点是在维护操作 2,3 的旋转。

可以发现对于操作 2,3 都很难在节点上打上标记,如果暴力去打会因为结点数太多而时间复杂度直线上升。又观察到层数不超过 19,且建出的线段树是一棵完全二叉树。所以我们可以将标记打到层上。由于同层有多个结点,所以标记用完不能删除,但是不删除有可能会被旋转两次以上。所以我们利用标记永久化的思想:遇到标记反向走(应走左子树去走右子树,应走右子树去走左子树)来模拟线段树结点的旋转。

记得操作 3 的标记要往上打一层,因为需要旋转的是这一层本身。

总时间复杂度 O(2n+qn)n18,包可过。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 20, M = 1e7 + 10;

struct Node {
  int ls, rs;
  int sum;
} t[M];

int n, q, idx, root, rev[N];

void pushup(int p) {
  t[p].sum = t[t[p].ls].sum + t[t[p].rs].sum;
}

void build(int &p, int l, int r) {
  p = ++idx;
  if(l == r) {
    cin >> t[p].sum;
    return ;
  }
  int mid = l + r >> 1;
  build(t[p].ls, l, mid);
  build(t[p].rs, mid + 1, r);
  pushup(p);
}

void upd(int p, int l, int r, int x, int k, int level) {
  if(l == r) {
    t[p].sum = k;
    return ;
  }
  int ls = t[p].ls, rs = t[p].rs;
  if(rev[level]) swap(ls, rs);
  int mid = l + r >> 1;
  if(x <= mid) upd(ls, l, mid, x, k, level-1);
  else upd(rs, mid + 1, r, x, k, level-1);
  pushup(p);
}

int ask(int p, int l, int r, int L, int R, int level) {
  if(L <= l && r <= R) {
    return t[p].sum;
  }
  int ls = t[p].ls, rs = t[p].rs;
  if(rev[level]) swap(ls, rs);
  int mid = l + r >> 1, ans = 0;
  if(L <= mid) ans += ask(ls, l, mid, L, R, level-1);
  if(R > mid) ans += ask(rs, mid + 1, r, L, R, level-1);
  return ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> n >> q;
  build(root, 1, (1<<n));
  while(q--) {
    int op; cin >> op;
    if(op == 1) {
      int x, k;
      cin >> x >> k;
      upd(root, 1, (1<<n), x, k, n);
    } else if(op == 2) {
      int k; cin >> k;
      For(i,0,k) rev[i] ^= 1;
    } else if(op == 3) {
      int k; cin >> k;
      rev[k+1] ^= 1;
    } else {
      int l, r; cin >> l >> r;
      cout << ask(root, 1, (1<<n), l, r, n) << '\n';
    }
  }
  return 0;
}

CF1322C Instant Noodles#

引理:在整数范围内,gcd(x,y,x+y)=gcd(x,y)

证明:可以找到一个整数 b 使得 b|x,b|y,且记所有可行的 b 中最大的数为 c,则有:

xy0(modc)

因为有 x0(modc),所以:

2xx+yx0(modc)

所以 x+y0(modc)

此时 c=gcd(x,y),因此得证 gcd(x,y,x+y)=gcd(x,y)

有了这一点理论,我们发现本题可以看成是求 ci 张成的线性空间中的最大公约数。

不妨将右部点看成左部点的映射,能观察到在右部点选择一个点等价于在左部点选择点集。贡献也能相应的算出。

由于右部点 ci 可张成线性空间,所以求任取 ci 的最大公约数和求所有 ci 的最大公约数是等价的。

最后只要注意合并相同的集合,并合并贡献即可。

集合用 set,映射用 map,总时间复杂度 O(nlogn)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e6 + 10;

int T, n, m, ans, c[N];

set<int> e[N];

map<set<int>, int> mp;

void solve() {
  ans = 0;
  map<set<int>, int>().swap(mp);
  For(i,1,n) set<int>().swap(e[i]);
  cin >> n >> m;
  For(i,1,n) cin >> c[i];
  For(i,1,m) {
    int u, v;
    cin >> u >> v;
    e[v].insert(u);
  }
  For(i,1,n) {
    if(e[i].size()) mp[e[i]] += c[i];
  }
  for (auto x : mp) {
    ans = __gcd(ans, x.second);
  }
  cout << ans << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1416B Make Them Equal#

没有第一眼看出来解法。

先判断无解:由于每一次操作涉及 +xixi,所以序列的总和不变。因此最后想要每个数都一样必须让这 n 个数的平均数填满 n 个空,所以总和 sum 不能被 n 整除则无解。

然后发现 i=1 的自由度是很高的(因为 1 是所有自然数的因数,可以很轻松的被放与拿)。所以大致的思路就是每一次将 ai 转移至 a1 上,然后再平均分配给每一个数即可。

转移的过程很简单,就是对于 ai 先凑 i 的倍数,凑的时候直接拿 a1 来用,凑完以后直接放回 a1。如果 ai 本身就是 i 的倍数了,直接放到 a1 即可。

a1 是一定够补的,因为第 i 个空最多要补 i1,而最坏的情况就是前 i1 个数都是 1,而它们现在都放在 a1 了,也就是正好能够补上 i 的空。

发现最后操作数不会超过 3(n1),总时间复杂度 O(n),符合题意,包能过。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e5 + 10;

struct Ans {
  int i, j, x;
};

int T, n, a[N];

void solve() {
  vector<Ans> ans;
  int sum = 0, cnt = 0;
  cin >> n;
  For(i,1,n) cin >> a[i], sum += a[i];
  if(sum % n != 0) {
    cout << "-1\n";
    return ; 
  }
  sum /= n;
  For(i,2,n) {
    if(a[i] % i == 0) {
      ans.push_back((Ans){i, 1, a[i] / i});
      continue;
    }
    int k = (int)(ceil(1.0 * a[i] / i) * i);
    ans.push_back((Ans){1, i, k - a[i]});
    ans.push_back((Ans){i, 1, k / i});
  }
  For(i,2,n) ans.push_back((Ans){1, i, sum});
  cout << ans.size() << '\n';
  for (int i = 0; i < ans.size(); ++i) cout << ans[i].i << ' ' << ans[i].j << ' ' << ans[i].x << '\n';
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1996E Decode#

题目要求统计的是每一个 (l,r) 中合法子串的数量,意味着暴力的来做需要枚举 l,r 以及其子串来统计答案。

可以想到一个子串会在不同的 (l,r) 中被统计多次。具体的,对于合法子串 (L,R) 来说,可以被 l[1,L],r[R,n] 所组成的 (l,r) 统计到。所以单个 [L,R] 的贡献为 L×(nR+1)

考虑如何快速统计合法字串 (L,R)。固定左端点 L,将原序列做前缀和,0 的贡献记为 11 的贡献记为 1,若前缀和数组中位置 i 出现了 A=0,则说明 (L,i) 为合法字串,可以计入贡献。当 L 移动,会使前缀和数组以 L 往后做全局加或减的操作。计入贡献的标准 A 也会随之变化。而 A 的变化和前缀和数组的变化同步进行,于是在操作开始之前将前缀和数组出现的数用桶存起来,统计答案时改变标准后将贡献累计答案即可。

由于前缀和可能出现负数,所以桶用 map 替代时间复杂度 O(nlogn)

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define mod 1000000007

using namespace std;

const int N = 2e5 + 10; 

int T, n, sum[N];

char s[N];

map<int, int> mp;

void solve() {
  int ans = 0;
  map<int, int>().swap(mp);
  cin >> (s + 1);
  n = strlen(s + 1);
  For(i,1,n) sum[i] = sum[i-1] + (s[i] == '0' ? -1 : 1);
  For(i,1,n) {
    mp[sum[i]] = (mp[sum[i]] + (n-i+1) % mod) % mod;
  }
  int A = 0;
  For(i,1,n) {
    ans = (ans + (i * mp[A]) % mod) % mod;
    mp[sum[i]] = (mp[sum[i]] - (n-i+1) % mod + mod) % mod;
    if(s[i] == '0') A--;
    else A++;
  }
  cout << ans << '\n';
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve(); 
  return 0;
}

CF1993C Light Switches#

很神奇的一道题。

考虑直接枚举位置 x 为答案,然后对所有 n 个点进行判断。第 i 个点的位置为 ai,若 ai 合法当且仅当 xai 的开灯范围之内,所以若 (xai)k 为偶数即合法。

还可以缩小一下答案枚举的范围,记 l,r 为答案合法范围。将 a 数组排序,初始来看答案只有可能出现在 [an,an+2k1] 这个范围之内,因为存在循环节,后面的所有答案有合法却不是最小的。对于每一个位置 i 都要枚举所有的合法区间,然后对于所有的区间取交集即可。

枚举 lr,然后判断合法即可。

时间复杂度 O((rl+1)×n)[l,r] 长度期望极小且严格跑不满。可以通过本题。

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)

using namespace std;

const int N = 2e5 + 10;

int T, n, k, a[N], ans;

void solve() {
  cin >> n >> k;
  For(i,1,n) cin >> a[i];
  sort(a + 1, a + n + 1);
  int l = a[n], r = a[n] + 2 * k - 1;
  For(i,1,n) {
    for (int j = 0; a[i] + k * j <= r && a[i] + (j + 1) * k - 1 >= l; j += 2) {
      r = min(r, a[i] + (j + 1) * k - 1);
      l = max(l, a[i] + k * j);
    }
  }
  For(i,l,r) {
    For(j,1,n) {
      if(((i - a[j]) / k) % 2 != 0) {
        goto yzy;
      }
    }
    cout << i << '\n';
    return ;
    yzy:;
  }
  cout << "-1\n";
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1970E1 Trails (Easy)#

很暴力的计数题。

dpi,j 表示在第 i 天,最后待在了 j 号房子中的总方案数。

先考虑从 i 号房间走到 j 号房间的方案数如何计算。不考虑限制,单次方案数为 (sj+lj)×(sk+lk),然后由于不能存在同时为长路径的情况,于是把 (lj×lk) 减掉即可。

枚举回合 i,起点 j,终点 k,于是有:

dpi,j=k=1mdpi1,k×max(0,(sj+lj)×(sk+lk)(lj×lk))

答案为 i=1mdpn,i,记得初始化 dp0,1=1

时间复杂度 O(nm2)

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007

using namespace std;

const int M = 105, N = 1005;

int n, m, s[M], l[M], dp[N][M];

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> m >> n;
  For(i,1,m) cin >> s[i];
  For(i,1,m) cin >> l[i];
  dp[0][1] = 1;
  For(i,1,n) {
    For(j,1,m) {
      For(k,1,m) {
        dp[i][j] += (dp[i-1][k] % mod * (max(0ll, (s[j] + l[j]) * (s[k] + l[k]) - (l[j] * l[k])) % mod)) % mod;
      } 
    } 
  }
  int ans = 0;
  For(i,1,m) {
    ans += dp[n][i];
    ans %= mod;
  }
  cout << ans << '\n';
  return 0;
}

CF1970E2 Trails (Medium)#

考虑到 n=109,无法在 O(nm2) 的时间复杂度下通过此题。

我们令 ri=si+li,则有:

dpi,j=k=1mdpi1,k×max(0,rjrkljlk)

发现式子有乘,有加,于是考虑构造矩阵进行加速。

[r1r1l1l1r1r2l1l2r1rml1lmr2r1l2l1r2r2l2l2r2rml2lmrmr1lml1rmr2lml2rmrmlmlm]

对此矩阵进行快速幂(迭代 n 次)的结果矩阵乘上

[dp1,1dp2,1dpm,1]

得到矩阵

[dp1,ndp2,ndpm,n]

答案便为 i=1mdpi,n,记得初始化矩阵 dp1,1=1

时间复杂度 O(m3logn)

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007

using namespace std;

const int MM = 105, N = 1005;

int n, m, s[MM], l[MM], ans, r[MM];

struct Matrix {
  int M[MM][MM];
  void clean() {
    For(i,1,m) For(j,1,m) M[i][j] = 0;
  }
  void init() {
    For(i,1,m) M[i][i] = 1; 
  }
  Matrix friend operator * (const Matrix a, const Matrix b) {
    Matrix Ans; Ans.clean();
    For(i,1,m) {
      For(j,1,m) {
        For(k,1,m) {
          Ans.M[i][j] = (Ans.M[i][j] + (a.M[i][k] % mod * b.M[k][j] % mod) % mod) % mod;
        }
      }
    }
    return Ans;
  }
};

Matrix qpow(Matrix a, int b) {
  Matrix Ans;
  Ans.init();
  for (; b; b >>= 1) {
    if(b & 1) Ans = Ans * a;
    a = a * a;
  }
  return Ans;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> m >> n;
  For(i,1,m) {cin >> s[i]; s[i] %= mod;}
  For(i,1,m) {cin >> l[i]; r[i] = (s[i] + l[i] % mod) % mod;}
  Matrix T, Ans;
  Ans.clean();
  Ans.M[1][1] = 1;
  For(i,1,m) {
    For(j,1,m) {
      T.M[i][j] = max(0ll, (r[i] * r[j]) - (l[i] * l[j]));
    }
  }
  Ans = qpow(T, n) * Ans;
  For(i,1,m) (ans += Ans.M[i][1]) %= mod;
  cout << ans << '\n';
  return 0;
}

CF1970E3 Trails (Hard)#

以上仍然过不了 m=105 的情况。考虑原矩阵等价于 A×B,其中:A=[r1l1r2l2rmlm]B=[r1r2rml1l2lm]

然后答案为 (AB)n 的第一列之和。

直接对矩阵 AB 进行快速幂肯定不可行,因为矩阵 ABm×m 的。考虑到矩阵乘法有结合律,于是就有 (AB)n=A×(BA)n1×B

第一次先对 2×2BA 矩阵进行快速幂,然后依次进行 A×(BA)n1A×(BA)n1×B 的运算。最后的矩阵大小为 m×m 所以最后一次矩阵乘法只要算第一列答案即可。

时间复杂度 O(m+mlogn)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007

using namespace std;

const int N = 1e5 + 10;

int n, m, s[N], l[N], r[N];

struct Matrix {
  int M[3][3];
  
  void clean() {
    For(i,1,2) For(j,1,2) M[i][j] = 0;
  }

  void init() {
    For(i,1,2) M[i][i] = 1;
  }

  void print() {
    For(i,1,2) {
      For(j,1,2) cout << M[i][j] << ' ';
      cout << endl;
    } 
  }

  Matrix friend operator * (const Matrix a, const Matrix b) {
    Matrix Ans;
    Ans.clean();
    For(i,1,2) {
      For(j,1,2) {
        For(k,1,2) {
          Ans.M[i][j] = (Ans.M[i][j] + (a.M[i][k] % mod * b.M[k][j] % mod) % mod) % mod;
        }
      }
    }
    return Ans;
  }
} Ans;

struct MATRIX1 {
  int M[3][N];
  void clean() {
    For(i,1,2) For(j,1,m) M[i][j] = 0;
  }
  void print() {
    For(i,1,2) {
      For(j,1,m) cout << M[i][j] << ' ';
      cout << endl;
    } 
  }
} B;

struct MATRIX2 {
  int M[N][3];
  void clean() {
    For(i,1,m) For(j,1,2) M[i][j] = 0;
  }
  void print() {
    For(i,1,m) {
      For(j,1,2) cout << M[i][j] << ' ';
      cout << endl;
    } 
  }
} A, Ans1, Ansend;

Matrix qpow(Matrix a, int b) {
  Matrix res; res.clean(); res.init();
  for (; b; a = a * a, b >>= 1) {
    if(b) res = res * a;
  }
  return res;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> m >> n;
  For(i,1,m) cin >> s[i], s[i] %= mod;
  For(i,1,m) cin >> l[i], l[i] %= mod, r[i] = (s[i] + l[i]) % mod;
  Ans.clean(); A.clean(); B.clean(); Ans1.clean(); Ansend.clean();
  For(i,1,2) For(j,1,m) B.M[i][j] = (i == 1 ? r[j] : (-l[j] + mod) % mod);
  For(i,1,m) For(j,1,2) A.M[i][j] = (j == 1 ? r[i] : l[i]);
  For(i,1,2) {
    For(j,1,2) {
      For(k,1,m) {
        Ans.M[i][j] = (Ans.M[i][j] + (B.M[i][k] % mod * A.M[k][j] % mod) % mod) % mod;
      }
    }
  }
  Ans = qpow(Ans, n-1);
  For(i,1,m) {
    For(j,1,2) {
      For(k,1,2) {
        Ans1.M[i][j] = (Ans1.M[i][j] + (A.M[i][k] % mod * Ans.M[k][j] % mod) % mod) % mod;
      }
    }
  }
  int ans = 0;
  For(i,1,m) {
    int j = 1;
    For(k,1,2) {
      ans = (ans + (Ans1.M[i][k] * B.M[k][j]) % mod) % mod;
    }
  }
  cout << ans << '\n';
  return 0;
}

CF1537D Deleting Divisors#

可以想到,能走到一个必败点的就是必胜点,这样可以写出来一个 O(n2) 的 dp。

然后考虑所有的 n 为奇数的情况。如果 n 为质数,则为必败点,如果 n 为合数,则一定能走到一个 n 为偶数的必败点。

然后考虑所有的 n 为偶数的情况。如果 n=2x 其中 x 为奇数,则为必败点。如果 n=2x 其中 x 为偶数,则为必胜点。其他偶数均为必胜点。

然后 O(1) 判断即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e7 + 10;

int n, T;
void solve() {
  cin >> n;
  if(n & 1) {
    cout << "Bob\n";
  } else if((1ll << (int)(ceil(__lg(n)))) == n) {
    if((int)(ceil(__lg(n))) & 1) cout << "Bob\n";
    else cout << "Alice\n";
  } else {
    cout << "Alice\n";
  }
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

CF1498D Bananas in a Microwave#

考虑暴力 dp。

fi,j 表示操作 i 中是否出现了 j。转移为:fi,j=fi1,j,再枚举操作数计算每一轮的 k(初始 fi1,j=1 才能转移),然后进行转移即可。

考虑优化枚举 j 后进行多轮计算 k 并转移的过程。发现每一轮的 k 是单调不减的,我们再记录一个 gi 表示在当前操作下,转移到 i 所需要的操作轮数。这样我们枚举 j 并且转移的过程可以变为线性的:计算一轮 k,每一次更新 gj=gk+1。下次转移到 xgx=y 则不能用 x 进行转移了。这样就能使时间复杂度降至 O(nm)

fi 表示转移至 i 的最小操作轮数,转移照常且方便输出答案。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e5 + 10;

int n, m, f[N], g[N];

signed main() {
  // ios::sync_with_stdio(0);
  // cin.tie(0), cout.tie(0);
  cin >> n >> m;
  memset(f, -1, sizeof f);
  f[0] = 0;
  For(i,1,n) {
    int t, x, y;
    cin >> t >> x >> y;
    memset(g, 0, sizeof g);
    double x0 = (1.0 * x) / 100000;
    For(j,0,m) {
      if(f[j] == -1 || g[j] == y) continue;
      if(t == 1) {
        int k = j + ceil(x0);
        if(k <= m) if(f[k] == -1) f[k] = i, g[k] = g[j] + 1; 
      } else {
        int k = ceil(j * (1.0 * x) / 100000);
        if(j && k <= m) if(f[k] == -1) f[k] = i, g[k] = g[j] + 1; 
      }
    }
  }
  For(i,1,m) cout << f[i] << ' ';
  return 0;
}

CF1976D Invertible Bracket Sequences#

思维题。

考虑括号序列的合法要求。记 "(" 为 1,")" 为 0,做前缀和 sumi。如果 sumi0j[1,i],sumj0,则区间 [1,i] 为合法括号序列。

考虑翻转一段区间之后判断括号系列是否合法,设翻转区间为 [l,r],翻转之后的前缀和 s,若 sumrsuml1=0,同时 mini=lrsi0 即为合法的翻转。

考虑 sumisi 的关系,si=suml1+sisl1=suml1(sumisuml1)=2suml1sumi,所以需满足 mini=lr2suml1sumi0,故可知 maxi=lrsumi2suml1。最后统计出满足上述条件下所有等于 suml1 的右端点个数即可。

枚举 l,由于合法的 r 具有单调性,用二分统计出个数即可。统计个数时可以用 vector 记录所有大小为 sumi 的位置 i。然后再用二分统计出在区间 [l,r] 中的个数加入答案即可。二分判断时 maxi=lrsumi 用 st 表维护即可。

时间复杂度 O(nlogn)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 2e5 + 10, M = 23;

int T, n, sum[N], idx, root[N], st[N][M];

char s[N];

map<int, vector<int> > pos;

int qry(int l, int r) {
  int k = __lg(r - l + 1);
  return max(st[l][k], st[r-(1<<k)+1][k]);
}

int query(int l, int r, int x) {
  return (upper_bound(pos[x].begin(), pos[x].end(), r) - lower_bound(pos[x].begin(), pos[x].end(), l));
}

void solve() {
  cin >> s + 1;
  n = strlen(s + 1);
  map<int, vector<int> >().swap(pos);
  For(i,1,n) {
    sum[i] = sum[i-1] + (s[i] == '(' ? 1 : -1);
    pos[sum[i]].push_back(i);
    st[i][0] = sum[i];
  }
  for (int j = 1; (1 << j) <= n; ++j) {
    for (int i = 1; i + (1 << j) - 1 <= n; ++i) {
      st[i][j] = max(st[i][j-1], st[i+(1<<(j-1))][j-1]);
    }
  }
  int ans = 0;
  For(i,1,n) {
    int l = i, r = n, R = i-1;
    while(l <= r) {
      int mid = l + r >> 1;
      if(qry(l, mid) <= 2 * sum[i-1]) {
        R = mid;
        l = mid + 1;
      } else {
        r = mid - 1;
      }
    }
    ans += query(i, R, sum[i-1]);
  }
  cout << ans << '\n';
  return ;
}

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> T;
  while(T--) solve();
  return 0;
}

作者:Daniel-yao

出处:https://www.cnblogs.com/Daniel-yao/p/18326784

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Daniel_yzy  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示