第一课 枚举法

枚举法是一种常用的问题求解方法。

对于求满足条件 的东西的数量或最大/最小值这类问题,一个自然且暴力的想法是把满足条件 的东西逐个枚举出来。

使用枚举法的难点在于

  • 枚举什么?
  • 枚举的范围?

例题: ABC 猜想 abc227_c

给你一个正整数

求满足下列条件的正整数三元组 的数量。

限制
  • 答案小于

样例输入 1

4

样例输出 1

5

满足条件的三元组有

样例输入 2

100

样例输出 2

323

样例输入 3

100000000000

样例输出 3

5745290566750

解法

  • 枚举 ,计算 的个数。

        • 的数量 =
  • 控制枚举量:

代码

int main() {
  long long ans = 0;
  long long n; cin >> n;
  long long cnt = 0;
  for (long long a = 1; a * a * a <= n; a++)
    for (long long b = a; a * b * b <= n; b++) {
      ans += n / a / b - b + 1;
    }
  cout << ans << '\n';
}

Tips

  • 乘法很快,你不需要 cbrt()

代码足够快吗?

用最大的数据 验证。

估算枚举量

最直接的办法:运行程序算枚举次数。

Tips

  • 利用约束条件控制枚举范围,不要“傻瓜式”枚举。
  • 用极限数据测运行时间。

例题: 等差数 abc234_e

我们把满足下列条件的正整数 称为等差数

  • 的十进制表示是 )。 成立。
    • 换言之, 是等差数列。
    • 特别地,一位数都是等差数。

例如 是等差数而 不是。

求不小于 的最小等差数。

限制
  • 是整数。

样例输入 1

152

样例输出 1

159

样例输入 2

88

样例输出 2

88

样例输入 3

8989898989

样例输出 3

9876543210

解法

  • 不小于 的最小等差数的位数跟 相同。(为什么?)

  • 的位数是

  • 解空间:长度 ,各项是 的数字,首项不是 ,的等差数列。

  • 等差数列由首项和公差确定。

    • 首项:
    • 公差:
  • 枚举量小于

代码

int main() {
  long long x;
  cin >> x;
  int len = to_string(x).size();
  long long ans = x * 10;
  for (int a = 1; a <= 9; a++) {
    for (int d = -9; d <= 9; d++) {
      long long sum = 0;
      for (int i = 0; i < len; i++) {
        int t = a + d * i;
        if (t < 0 || t > 9) break;
        sum = sum * 10 + t;
      }
      if (sum >= x) {
        ans = min(sum, ans);
      }
    }
  }
  cout << ans << '\n';
}

Tips

  • 利用解的性质(明示的和隐含的)精准枚举。

例题:M <= ab abc296_d

给你两个正整数

找到满足下列两条件的最小正整数 。若不存在这样的整数,输出

  • 可以表示为两个整数 的乘积且 。这里, 可以相同。
限制

我们要找两个数 满足

越小越好。

观察

  • 如果 确定了, 就能确定,
  • 越小越好。 中较小的那个不超过

解法

  • 枚举 ,从 ,算出

代码

int main() {
  long long N, M;
  cin >> N >> M;
  long long ans = -1;
  for (long long a = 1; a <= N; a++) {
    long long b = (M + (a - 1)) / a;
    if (b <= N) {
      if (ans == -1 || ans > a * b)
        ans = a * b;
    }
    if (a * a >= M)
      break;
  }
  cout << ans;
  return 0;
}

Tips

  • 对于两数相乘如何如何这种条件,通常去考虑两数当中较小的那个数的上界。

例题:校庆

给定正整数 ,求 在几进制下的写法末尾的 0 最多。

输出两个数 ,表示 进制下末尾的 0 最多,有 个;若有多个 满足条件,输出最大的那个。

限制

分析

  • 进制下末尾的 0 的个数 = 能被 整除的次数。
  • 进制下写成 10,末尾有一个 0。
  • 我们试图找一个 使得 进制下末尾至少有两个 0,也就是
  • 我们可以枚举 ,从 ;可是 太大,这样会超时。
  • 我们枚举 ,从 。对于大于 ,注意到 ,我们可以通过枚举 来找到

Tips

枚举因数类问题的一个常用技巧:

  • 正整数 的因数是成对的:如果 的因数,那么 也是 的因数; 越大, 就越小。

在《校庆》这道题中,我们要枚举 的因数 。当 比较小时,我们直接枚举 ;当 比较大时,我们转而去枚举较小的

代码

void solve(long long n) {
  int C = 1;
  long long B = n;
  for (long long b = 2; b * b * b <= n; b++) {
    if (n % (b * b) == 0) {
      long long t = n / (b * b);
      int cnt = 2;
      while (t % b == 0) {
        t /= b;
        cnt++;
      }
      if (cnt >= C) {
        C = cnt;
        B = b;
      }
    }
  }
  if (C <= 2) // 当 b > n**{1/3} 时,n 在 b 进制下末尾最多有 2 个 0
    for (int k = 1; k * k * k < n; k++)
      if (n % k == 0) {
        long long t = sqrt(n / k);
        if (t * t == n / k) {
          C = 2;
          B = t;
          break;
        }
      }
  cout << B << ' ' << C << '\n';
}

例题:数字和 arc060_b

对于整数 )和 ),定义函数 写成 进制时的数字和。例如

给定整数 。判断是否存在整数 )使得 。若存在,找到满足条件的最小的

限制

解法

对于 ,暴力枚举 。对于 ,注意到 进制下最多是两位数,且第二位上的数字小于 ,此时可以暴力枚举第二位上的数字 ,那么第一位上的数字就是 ,然后检查是否有

例题:立方差 abc397_d

给你一个正整数 。判断是否有一对正整数 使得 。若有,输出一组解,否则输出 -1。

限制

推式子

分解因式

注意到

所以

我们得到

枚举 x - y

,枚举 ,从

然后怎么办?

办法一

代入方程 ,得

解此二次方程就得到

其实解这个方程也不是很容易。

办法二

我们有

两式相减,得

那么

开平方根就得到

有了 就能算出

代码

void solve(long long N) {
  for (long long d = 1; d * d * d < N; d++) {
    if (N % d) continue;
    long long k = N / d - d * d;
    if (k % 3) continue;
    long long xy = k / 3;
    long long t = d * d + 4 * xy;
    long long s = sqrt(t);
    if (s * s != t) continue;
    // x + y = s;     x - y = d
    long long x = (s + d) / 2;
    long long y = s - x;
    cout << x << ' ' << y << '\n'; return;
  }
  cout << -1 << '\n';
}

例题:罐头和开罐器 abc312_f

个物品。每个物品是易拉罐头,普通罐头,或开罐器。

个物品由一对整数 描述:

  • ,第 个物品是易拉罐头;吃了它,你会获得 点体力。
  • , 第 个物品是普通罐头,需要使用开罐器打开;吃了它,你会获得 点体力。
  • , 第 个物品是开罐器;它可以开罐 次。

你可以从 个物品中拿走 个。求最多能获得多少点体力。

限制

观察

  • 每种东西,不论拿几个,都是优先拿 值大的。
  • 把“拿 个物品”改为“拿不超过 个物品”,答案不变。
  • 加一个限制:拿的普通罐头都打开吃掉了。(没吃掉的干脆不拿。)
  • 拿了几个普通罐头决定了需要拿几个开罐器。

解法

  • 把物品的 值按物品种类分组存放。
  • 每种物品的 值从大到小排序。
  • 计算每种物品的 值的前缀和数组。
  • 枚举拿几个普通罐头,然后确定拿几个开罐器,然后确定拿几个易拉罐头。

代码

int main() {
    int n, m; cin >> n >> m;
    vector<int> X[3];
    for (int i = 0; i < n; i++) {
        int t, x; cin >> t >> x;
        X[t].push_back(x);
    }
    for (int i = 0; i < 3; i++)
        sort(X[i].rbegin(), X[i].rend());
    
    vector<long long> sum[3]; // 前缀和
    for (int i = 0; i < 3; i++) {
        sum[i].resize(X[i].size() + 1);
        for (int j = 0; j < X[i].size(); j++)
            sum[i][j + 1] = sum[i][j] + X[i][j];
    }

    long long ans = 0;
    int j = 0; // 需要的开罐器的数量
    for (int i = 0; i <= (int) X[1].size(); i++) {
        if (i > m) break;
        if (sum[2][j] < i) j++;
        if (j > (int) X[2].size() || i + j > m) break;
        int k = min(m - i - j, (int) X[0].size()); // 拿的易拉罐的数量
        ans = max(ans, sum[0][k] + sum[1][i]);
    }
    cout << ans << '\n';
}

Tips

有时候一个变量确定了,其他变量都确定了。

例题:偶数矩阵 UVa 11464

给你一个 的 01 矩阵(每个元素非 0 即 1),你的任务是把尽量少的 0 变成 1,使得矩阵中每个元素的上、下、左、右的元素(如果存在的话)之和均为偶数。

组数据。对于每组数据,输出被改变的元素的最小个数。如果无解,输出

限制

例题:护城河 abc129_e

坐标平面上有一些村庄。我们可以把一个村庄当成一个点。
我们要挖护城河来保护这些村庄。

给你一个 矩阵 ,矩阵的每一项是 。对于每一对整数 ),若 ,则点 处有一个村庄。

可以把护城河看成平面上的一个多边形。这个多边形要满足下列条件。

  1. 多边形的边不自交。
  2. 所有村庄都位于多边形内部。
  3. 多边形的每个顶点的 坐标和 坐标都是 的整数(包括 )。
  4. 多边形的每条边都平行于 轴或 轴。
  5. 多边形的每个内角是 度或 度。

求挖护城河有多少种方式。

样例输入 1

1 0 0 0
0 0 1 0
0 0 0 0
1 0 0 0

样例输出 1

1272

下列两种护城河满足条件。

下列四种护城河不满足条件。

思路

  • 枚举护城河(封闭的折线)不容易。
  • 我们枚举被护城河包围的格子是哪些(从 个格子里选一些,一共 种情况)然后护城河就唯一确定了。
  • 被护城河包围的格子需要满足:连通且没有洞。

如何判断是否有洞?

判断是否有洞

  • 从最外面一圈未被选中的格子开始搜索四连通的未被选中的格子。若所有未被选中的格子都能索到,说明没有洞。

代码

int a[4][4];
int vis[4][4];

int main() {
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      cin >> a[i][j];
  int ans = 0;
  for (int s = 1; s < 1 << 16; s++) {
    memset(vis, 0, sizeof vis);
    for (int i = 0; i < 16; i++) {
      if (s >> i & 1) {
        vis[i / 4][i % 4] = 1; //给选中的格子打标记
      }
    }
    ans += check();
  }
  cout << ans << '\n';
}
int dir[4][2] = {1, 0, -1, 0, 0, 1, 0, -1};

void dfs(int r, int c, int type) { // 搜连通块
  vis[r][c]++;
  for (int i = 0; i < 4; i++) {
    int x = r + dir[i][0], y = c + dir[i][1];
    if (0 <= x && x < 4 && 0 <= y && y < 4 && vis[x][y] == type)
      dfs(x, y, type);
  }
}
int check() { // 检查
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      if (a[i][j] == 1 && vis[i][j] == 0) //没有选择全部村庄
        return 0; 
  int sx, sy;
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      if (vis[i][j]) {
        sx = i;
        sy = j;
      }
  dfs(sx, sy, 1);
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      if (vis[i][j] == 1)
        return 0;// 选中的格子不连通
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      if (vis[i][j] == 0 && (i == 0 || j == 0 || i == 3 || j == 3))
        dfs(i, j, 0);
  for (int i = 0; i < 4; i++)
    for (int j = 0; j < 4; j++)
      if (vis[i][j] == 0) // 选中的格子有洞
        return 0;
  return 1;
}

Tips

自顶向下、逐步求精的模块化程序设计。

例题:假期计划 P8817

给定有 个点 条边的无向图 ,点从 编号。对 ,点 有分数 。今要从点 中选择四个相异的点 ,使得序列 中每相邻两点在图 上距离都不大于

四点的分数之和的最大值。

保证存在满足条件的四个点。

限制

思路

  • 观察右图,注意到行程的对称性 对称, 对称。
  • 考虑枚举中间两点
  • 考虑 的关系( 的关系也是这样):
    • 到点 的距离都不超过
  • 最优解中, 两点也可能与 有上述关系。
  • 无论 两点为何, 一定在和 有上述关系的点中,分数前三大者当中。

此思路也可说是 meet in the middle。

解法

  • 对每个点
    • 找出在距离点 和点 都不超过 的那些点。
    • 取其中分数前三大的三个点,成集合
  • 枚举 ,从 中选一点作为 ,从 中选一点作为

Tips

  • 对称性十分重要。
const int maxn = 2500 + 5;
int n, m, k;
vector<int> g[maxn];
long long p[maxn]; //景点的分数
vector<int> cand[maxn];
int dist[maxn][maxn];
const int INF = 1e9;

void bfs(int s) {
  for (int i = 1; i <= n; i++)
    dist[s][i] = INF;
  queue<int> q;
  q.push(s);
  dist[s][s] = 0;
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (int v : g[u])
    if (dist[s][v] == INF) {
      dist[s][v] = dist[s][u] + 1;
      q.push(v);
    }
  }
}

bool cmp(int i, int j) {
  return p[i] > p[j];
}
int main() {
  cin >> n >> m >> k;
  for (int i = 2; i <= n; i++) cin >> p[i];
  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++) bfs(i);

  vector<int> ad;
  for (int i = 2; i <= n; i++)
    if (dist[1][i] <= k + 1) ad.push_back(i);
  sort(ad.begin(), ad.end(), cmp);
  
  for (int i = 2; i <= n; i++)
    for (int v : ad)
      if (v != i && dist[i][v] <= k + 1) {
        cand[i].push_back(v);
        if (cand[i].size() == 3) break;
      }

  long long ans = 0;

  for (int b = 2; b <= n; b++)
    for (int c = b + 1; c <= n; c++)
      if (dist[b][c] <= k + 1)
        for (int a : cand[b])
          for (int d : cand[c])
            if (a != c && a != d && b != d)
              ans = max(ans, p[a] + p[b] + p[c] + p[d]);
  cout << ans << '\n';         
}

例题:找四元环 abc260_f

有一个简单无向图 ,图上有 个点和 条边。点和边都从 开始编号。

编号为 的点之间没有边,编号为 的点之间也没有边。

一个长度为 的环称为四元环。

若图 里含有四元环,从中任选一个,输出环上的四个点的编号(顺序任意)。若图 里不含有四元环,输出 -1。

样例

输入

输出

1 7 2 9

一个二分图,左边有 (≤ 三十万)个点,右边有 (≤ 三千)个点。要在上面找一个四元环。

观察

  • 右边的点数显著少于左边。
  • 二分图上的四元环形如右图。

观察

  • 我们要找两个左边的点 a, b 和两个右边的点 c, d 使得 a 和 c, d 都有边相连,b 和 c, d 都有边相连。
  • 右边的点最多 个。点对 c, d 的选择至多 个,不很多。

解法

alt text

alt text

时间:

鸽笼原理

代码

vector<int> a[300005];
int vis[3005][3005];

int main() {
  int S, T, M;
  cin >> S >> T >> M;
  while (M--) {
    int u, v;
    cin >> u >> v;
    a[u].push_back(v - S);
  }
  for (int u = 1; u <= S; u++) {
    for (int i = 0; i < a[u].size(); i++)
      for (int j = 0; j < i; j++) {
        int x = min(a[u][i], a[u][j]);
        int y = max(a[u][i], a[u][j]);
        if (vis[x][y] == 0) {
          vis[x][y] = u;
        } else {
          cout << vis[x][y] << ' ' << u << ' ' << x + S << ' ' << y + S;
          return 0;
        }
      }
  }
  cout << -1;
}

Tips

  • 仔细看数据范围。异常的数据范围往往暗示解法。
  • 画图观察。

总结

许多问题不是需要用什么算法才能解决的,好好枚举就足够了。