根号分治

对于一个问题,若规模较小,采用一种算法,若规模较大,采用另一种算法。

预备知识:基本不等式

是正实数。则 的算术平均数大于等于 的几何平均数,即

等号只在 时成立。

证明

推论

设实数 。函数 )在 处取到最小值

abc259_h Yet Another Path Counting

我们有一个 的网格。第 行第 列的格子里有一个整数

考虑从某个格子开始,向右或向下走到相邻的格子里若干(可以是零)次所得的路径。求这样的路径中,满足起点和终点格子里的数相同的路径的数量,模

两条路径不同,若它们经过的格子(括起点和终点格子)的集合不同。

限制

解法

把格子按照里面的数分类。把写有整数 的格子称为“-格子”。对于 ,设 -格子有 个。

设定一个阈值 。对每个

  • ,枚举每一对 -格子 ,若 ,算一个组合数 。时间
  • ,在整个网格上做一次递推。令 为从某个 -格子到格子 的路径的数量之和。有递推式 。时间

对所有满足 ,我们考虑和 。有

满足 不超过 个,所以这一部分的总时间是

我们考虑 的取值让 最小。我们取 ,总时间就是

代码

const int mod = 998244353;
int a[401][401], dp[401][401], C[800][400];
vector<pair<int, int>> pos[160000 + 5];

int main() {
  int n; cin >> n;
  for (int i = 0; i < 2 * n; i++) C[i][0] = 1;
  for (int i = 1; i < 2 * n; i++)
    for (int j = 1; j <= min(n - 1, i); j++)
      C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
  
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= n; j++) {
      cin >> a[i][j];
      pos[a[i][j]].push_back({i, j});
    }

  long long ans = 0;
  for (int i = 1; i <= n * n; i++) {
    if (pos[i].size() > n) {
      for (int j = 1; j <= n; j++)
        for (int k = 1; k <= n; k++) {
          dp[j][k] = (dp[j - 1][k] + dp[j][k - 1]) % mod;
          if (a[j][k] == i) {
            dp[j][k]++;
            ans += dp[j][k];
          }
        }
    } else {
      for (int j = 0; j < pos[i].size(); j++)
        for (int k = j; k < pos[i].size(); k++) {
          int dx = pos[i][k].first - pos[i][j].first;
          int dy = pos[i][k].second - pos[i][j].second;
          if (dx >= 0 && dy >= 0) ans += C[dx + dy][dx];
        }
    }
  }
  cout << ans % mod << '\n';
}

模型

集合 含有 个元素。 的一个划分。我们要计算

性质: 有两种算法

  • 法一:时间是
  • 法二:时间是

取一阈值 。若 用法一计算 ,若 用法二计算

法一的总时间是 ,法二的总时间是

,总时间是

abc365_g AtCoder Office

个人在 AtCoder 办公式工作。办公室记录人员进出,至今已有 条记录。

)条记录是一对整数 ,表示在 时刻第 个人进或出办公室。

已知开始记录时所有人都在办公室外,现在所有人也都在办公室外。

回答 个询问,格式如下。

)个询问给你两个整数 。求自开始记录以来,第 和第 个人都在办公室的总时长。

限制
  • 输入的值都是整数。

解法

一共有 个区间。取阈值 。对于询问

  • 两人的区间都不超过 个,暴力处理这个询问,时间是 。处理所有这样的询问的总时间是

  • 否则不妨设 的区间超过 个,这样的人不超过 个。对每个这样的人 ,用累积和交错和这两个技巧可以在 时间内预处理出其余每个人跟 都在办公室的时长:

    • 设从 时刻到 时刻, 一直在办公室。那么
      时段内 两人都在办公室的时长 = 时段内 在办公室的时长 = 截至 时刻 在办公室内的累积时长 - 截至 时刻 在办公室内的累积时长。

    回答询问时查表即可。这样的预处理的总时间是

总时间是 ,取 ,总时间就成为

代码

void solve() {
  int n, m; cin >> n >> m;
  vector<pair<int,int>> rec(m);
  for (int i = 0; i < m; i++) cin >> rec[i].first >> rec[i].second;
  vector<vector<pair<int,int>>> seg(n + 1);
  vector<int> last(n + 1);
  for (auto [t, p] : rec)
    if (!last[p]) last[p] = t;
    else {
      seg[p].push_back({last[p], t});
      last[p] = 0;
    }

  int lim = 1000;

  unordered_map<int, vector<int>> ans;

  for (int i = 1; i <= n; i++) {
    if (seg[i].size() > lim) {
      vector<int> alt_sum(n + 1);// 交错和
      int sum_i = 0;  // cumulative sum of time when person i was in office
      bool in = false;// is person i in office now?
      int prev_i = 0; // the last time when person i entered or exited office
      for (auto [t, p] : rec) {
        if (p == i) {
          sum_i += in * (t - prev_i);
          prev_i = t;
          in ^= 1;
        }
        alt_sum[p] = sum_i + in * (t - prev_i) - alt_sum[p];
      }
      ans[i] = alt_sum;
    }
  }
  int q; cin >> q;
  while (q--) {
    int a, b; cin >> a >> b;
    if (seg[a].size() > lim)
      cout << ans[a][b] << '\n';
    else if (seg[b].size > lim)
      cout << ans[b][a] << '\n';
    else {
      int t = 0, i = 0;
      for (auto [l, r] : seg[a]) {
        while (i < seg[b].size() && seg[b][i].second <= r) {
          t += clamp(seg[b][i].second, l, r) - clamp(seg[b][i].first, l, r);
          i++;
        }
        if (i < seg[b].size()) // seg[b][i].second > r
          t += clamp(seg[b][i].second, l, r) - clamp(seg[b][i].first, l, r);
      }
      cout << t << '\n';
    }
  }
}

模型

集合 含有 个元素。 的一个划分。

有一个函数 ),满足

回答 个询问,每个询问给你两个整数 ),求

性质

计算 有两种方法:
法一: 在 时间内计算
法二: 对于 ,在 时间内算出所有的

取一个阈值 。对于满足 的子集 ,我们用法二预处理出所有 。这样的 不超过 个。总时间是 ,空间是

对于询问

  • 都不大于 ,则用法一计算 ,时间 。这样的询问最多 个,总时间
  • 大于 ,则查表。

总时间是 。取 ,总时间是

abc219_g Propagation(传播)

给你一个有 个点和 条边的简单无向图,点从 编号,点 上写有整数

给你 个询问。对每个 ,第 个询问给你整数 )。这个询问要你做下述操作。

  • 设写在点 上的整数是 。把点 的每个邻点上所写的整数改为

求依次处理所有询问后,每个点上所写的整数。

限制

解法

如果每个点的度都不大,那么暴力更新是可承受的。

注意到所有点的度之和是
取阈值 ,若点 的度大于 ,称 重点,否则称为轻点

对于第 个询问,首先设法求出当前写在点 上的数。然后

  • 是轻点,更新 的邻点,记下这些点最近一次被更新是在第 个询问;
  • 否则不更新 的邻点,只记下点 最近一次做传播中心是在第 个询问,和这时点 上的数。

问题是怎么求出当前写在点 上的数。

  • 枚举与 相邻的重点,这样的点至多有 个。找出这些点最晚成为传播中心的时间,看是否晚于当前记录的点 上一次被更新的时间。

我们取 。这样就能在 时间内处理一个询问。

代码

const int maxn = 2e5 + 5;
vector<int> g[maxn];
int last_updated[maxn];
int last_propagate[maxn];
int val[maxn];
int B;

int get_current_val(int u) {
  int t = last_updated[u];
  for (int v : g[u]) {
    if (g[v].size() <= B) break;
    t = max(t, last_propagate[v]);
  }
  return t == 0 ? u : val[t];
}

bool cmp(int u, int v) {
  return g[u].size() > g[v].size();
}
void solve() {
  int n, m, q; cin >> n >> m >> q;
  for (int i = 0; i < m; i++) {
    int u, v; cin >> u >> v;
    g[u].push_back(v);
    g[v].push_back(u);
  }
  for (int i = 1; i <= n; i++)
    sort(g[i].begin(), g[i].end(), cmp);
  B = sqrt(2 * m);
  for (int i = 1; i <= q; i++) {
    int x; cin >> x;
    val[i] = get_current_val(x);
    last_propagate[x] = i;
    if (g[x].size() <= B)
      for (int v : g[x])
        last_updated[v] = i;
  }
  for (int i = 1; i <= n; i++)
    cout << get_current_val(i) << ' ';
  cout << '\n';
}