AtCoder Beginner Contest 259 题解
E - LCM on Whiteboard
题意
给 N N N 个数 { a i } i = 1 N \{a_i\}_{i=1}^N {ai}i=1N,以 a i = p i , 1 e i , 1 × ⋯ × p i , m i e i , m i a_i=p_{i,1}^{e_{i,1}}\times\cdots\times p_{i,m_i}^{e_{i,m_i}} ai=pi,1ei,1×⋯×pi,miei,mi 的形式给出,即给出每个数的质因数分解形式。将其中一个数替换为 1,求出这时 N N N 个数的最小公倍数,对于 N N N 种替换情况,得到 N N N 个最小公倍数,问有多少个不同的最小公倍数。
题解
设所有 a i a_i ai 保持不变时的最小公倍数为 L L L,设将 a i a_i ai 替换为 1 时的最小公倍数为 L i L_i Li,有性质:当且仅当 a i a_i ai 的质因子分解式中存在在所有质因子中唯一的质因子满足该质因子的指数最大时, L i ≠ L L_i\ne L Li=L。由上述充要条件中的唯一性, L i ≠ L j , ∀ i ≠ j L_i\ne L_j,\forall i \ne j Li=Lj,∀i=j。所以只需将所有 a i a_i ai 的质因子分解式排列出来,并按质因子大小排序,若存在某个单独的质因子或者某个质因子的最大指数只有一个时,该质因子对应的 L i ≠ L L_i\ne L Li=L,从而答案加 1。
代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <array>
#include <numeric>
using i64 = long long;
void solve() {
int n;
std::cin >> n;
std::vector<std::vector<std::pair<int, int>>> a(n);
std::vector<std::array<int, 3>> all;
for (int i = 0; i < n; i++) {
int foo;
std::cin >> foo;
a[i].resize(foo);
for (int j = 0; j < foo; j++) {
std::cin >> a[i][j].first >> a[i][j].second;
all.push_back({ a[i][j].first, a[i][j].second, i });
}
}
sort(all.begin(), all.end());
std::vector<int> is(n);
for (int i = 0; i < all.size(); i++) {
if (i == all.size() - 1 || all[i][0] != all[i + 1][0]) {
if (i == 0 || all[i][0] != all[i - 1][0] || all[i][1] != all[i - 1][1]) {
is[all[i][2]] = 1;
}
}
}
int ans = 1 + std::accumulate(is.begin(), is.end(), 0);
std::cout << std::min(ans, n);
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
solve();
return 0;
}
F - Select Edges
题意
给定一棵树,树有边权,每个点有一个限制 d i d_i di,要从树中选边,要求点 i i i 连接的被选到的边数不能超过 d i d_i di,使得选到的边的边权和最大。
题解
考虑树形 DP,设 dp[i][0/1]
分别为考虑以 i 号点为根时,选的连到 i 和 i 的儿子的边数小于等于以及严格小于
d
i
d_i
di 时的答案。
我们现在固定一个根 u u u,考虑当已知 u u u 对应的子树(不含 u u u)的所有 dp 值都已知的情况,求 u u u 的 dp 值。对于 u u u 的儿子 v i v_i vi, ( u , v i ) (u,v_i) (u,vi) 这条边要么选要么不选,对应 v i v_i vi 的子树和这条边的答案为 S i S_i Si。如果选了 ( u , v i ) (u,v_i) (u,vi),则 S i = d p [ v i ] [ 1 ] + w i S_i=dp[v_i][1]+w_i Si=dp[vi][1]+wi;如果不选 ( u , v i ) (u,v_i) (u,vi),则 S i = max ( d p [ v i ] [ 0 ] , d p [ v i ] [ 1 ] ) S_i=\max(dp[v_i][0],dp[v_i][1]) Si=max(dp[vi][0],dp[vi][1])。初始情况是每条边都不选即第二种情况,然后考虑选哪些边收益变化最大,将变化量从大到小,贪心地选即可,当然选的数量不能超过 d [ u ] d[u] d[u]。
代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <array>
#include <numeric>
#include <functional>
using i64 = long long;
void solve() {
int n;
std::cin >> n;
std::vector<int> d(n);
for (int i = 0; i < n; i++) {
std::cin >> d[i];
}
std::vector<std::vector<std::pair<int, int>>> g(n);
for (int i = 0; i < n - 1; i++) {
int x, y, z;
std::cin >> x >> y >> z;
--x; --y;
g[x].emplace_back(y, z);
g[y].emplace_back(x, z);
}
const i64 INF = (i64)1e18;
std::vector<std::vector<i64>> dp(n, std::vector<i64>(2, -INF));
std::function<void(int, int)> dfs = [&](int u, int p) {
std::vector<i64> a;
i64 sum = 0;
for (auto& edge : g[u]) {
int v = edge.first, w = edge.second;
if (v == p) continue;
dfs(v, u);
i64 x = std::max(dp[v][0], dp[v][1]);
i64 y = dp[v][1] + w;
if (y > x) a.push_back(y - x);
sum += x;
}
sort(a.rbegin(), a.rend());
int cnt = std::min((int)a.size(), d[u]);
dp[u][0] = sum + std::accumulate(a.begin(), a.begin() + cnt, 0LL);
if (d[u] > 0) {
cnt = std::min((int)a.size(), d[u] - 1);
dp[u][1] = sum + std::accumulate(a.begin(), a.begin() + cnt, 0LL);
}
};
dfs(0, -1);
std::cout << std::max(dp[0][0], dp[0][1]);
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
solve();
return 0;
}
G - Grid Card Game
题意
给定一个 H × W H\times W H×W 的整数矩阵,需要从中选择若干行,若干列,要求行列交叉点不能为负数,得分为选中的格子分数之和,求最大得分。
题解
最开始先把行和与列和非负的每行每列都选了,然后考虑删除一些行和列。对于 a i , j ≥ 0 a_{i,j}\ge0 ai,j≥0,至少要减掉一次,因为该格子所在行和列都选了的话,就会重复计算一次,那也可以把该格子所在的一整行或一整列不选其一或者两者都不选;对于 a i , j < 0 a_{i,j}<0 ai,j<0,不能同时选该格子所在行和所在列,故需要删掉一整行或一整列,或两者都删。
由上述分析,考虑采用网络流最小割模型。源点 s s s,汇点 t t t, R 1 , R 2 , ⋯ , R H , C 1 , C 2 , ⋯ , C W R_1,R_2,\cdots,R_H,C_1,C_2,\cdots,C_W R1,R2,⋯,RH,C1,C2,⋯,CW,源点与 R i R_i Ri 连边,容量为 max { r i , 0 } \max\{r_i,0\} max{ri,0},其中 r i r_i ri 为第 i i i 行的和; C j C_j Cj 与汇点连边,容量为 max { c j , 0 } \max\{c_j,0\} max{cj,0}, c j c_j cj 为第 j j j 行的和, R i R_i Ri 与 C j C_j Cj 连边,容量为 (如果 a i , j ≥ 0 : a i , j a_{i,j}\ge0:a_{i,j} ai,j≥0:ai,j,否则 ∞ \infty ∞)。这样初始 a n s ans ans 为列和与行和非负的每行每列之和,然后求上述网络流的最小割,然后用 a n s ans ans 减去最小割即为答案。
代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <array>
#include <numeric>
#include <functional>
#include <assert.h>
using i64 = long long;
template<typename T>
class flow_graph {
public:
static constexpr T eps = (T)1e-9;
struct edge {
int from;
int to;
T c;
T f;
};
std::vector<std::vector<int>> g;
std::vector<edge> edges;
int n;
int st;
int fin;
T flow;
flow_graph(int _n, int _st, int _fin) : n(_n), st(_st), fin(_fin) {
assert(0 <= st && st < n && 0 <= fin && fin < n && st != fin);
g.resize(n);
flow = 0;
}
void clear_flow() {
for (const edge& e : edges) {
e.f = 0;
}
flow = 0;
}
int add(int from, int to, T forward_cap, T backward_cap) {
assert(0 <= from && from < n && 0 <= to && to < n);
int id = (int)edges.size();
g[from].push_back(id);
edges.push_back({ from, to, forward_cap, 0 });
g[to].push_back(id + 1);
edges.push_back({ to,from,backward_cap,0 });
return id;
}
};
template<typename T>
class dinic {
public:
flow_graph<T>& g;
std::vector<int> ptr;
std::vector<int> d;
std::vector<int> q;
dinic(flow_graph<T>& _g) : g(_g) {
ptr.resize(g.n);
d.resize(g.n);
q.resize(g.n);
}
bool expath() {
std::fill(d.begin(), d.end(), -1);
q[0] = g.fin;
d[g.fin] = 0;
int beg = 0, end = 1;
while (beg < end) {
int i = q[beg++];
for (int id : g.g[i]) {
const auto& e = g.edges[id];
const auto& back = g.edges[id ^ 1];
if (back.c - back.f > g.eps && d[e.to] == -1) {
d[e.to] = d[i] + 1;
if (e.to == g.st) {
return true;
}
q[end++] = e.to;
}
}
}
return false;
}
T dfs(int v, T w) {
if (v == g.fin) {
return w;
}
int& j = ptr[v];
while (j >= 0) {
int id = g.g[v][j];
const auto& e = g.edges[id];
if (e.c - e.f > g.eps && d[e.to] == d[v] - 1) {
T t = dfs(e.to, std::min(e.c - e.f, w));
if (t > g.eps) {
g.edges[id].f += t;
g.edges[id ^ 1].f -= t;
return t;
}
}
j--;
}
return 0;
}
T max_flow() {
while (expath()) {
for (int i = 0; i < g.n; i++) {
ptr[i] = (int)g.g[i].size() - 1;
}
T big_add = 0;
while (true) {
T add = dfs(g.st, std::numeric_limits<T>::max());
if (add <= g.eps) {
break;
}
big_add += add;
}
if (big_add <= g.eps) {
break;
}
g.flow += big_add;
}
return g.flow;
}
std::vector<bool> min_cut() {
max_flow();
std::vector<bool> ret(g.n);
for (int i = 0; i < g.n; i++) {
ret[i] = (d[i] != -1);
}
return ret;
}
};
void solve() {
int h, w;
std::cin >> h >> w;
std::vector<std::vector<i64>> a(h, std::vector<i64>(w));
std::vector<i64> r(h);
std::vector<i64> c(w);
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
std::cin >> a[i][j];
r[i] += a[i][j];
c[j] += a[i][j];
}
}
flow_graph<i64> g(h + w + 2, h + w, h + w + 1);
i64 ans = 0;
for (int i = 0; i < h; i++) {
i64 cap = std::max(0LL, r[i]);
g.add(g.st, i, cap, 0LL);
ans += cap;
}
for (int i = 0; i < w; i++) {
i64 cap = std::max(0LL, c[i]);
g.add(h + i, g.fin, cap, 0LL);
ans += cap;
}
const i64 INF = (i64)1e15;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
i64 cap = (a[i][j] < 0 ? INF : a[i][j]);
g.add(i, h + j, cap, 0);
}
}
dinic<i64> d(g);
ans -= d.max_flow();
std::cout << ans;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
solve();
return 0;
}
Ex - Yet Another Path Counting
题意
给定 N × N N\times N N×N 的方格,每个方格上有个标签(整数) a i , j a_{i,j} ai,j,其中 1 ≤ a i , j ≤ N 2 , 1 ≤ N ≤ 400 1\le a_{i,j}\le N^2,1\le N\le 400 1≤ai,j≤N2,1≤N≤400,一条合法路径被定义为从一个方格出发,只能向下走和向右走,起点和终点数字相同,求合法路径数。
题解
首先容易得到当起点终点固定时的向下向右走的路径条数。
方法一:考虑标签 i i i,其所在的格子有 ( x 1 , y 1 ) , ⋯ , ( x n i , y n i ) (x_1,y_1),\cdots,(x_{n_i},y_{n_i}) (x1,y1),⋯,(xni,yni),则可在 O ( n i 2 ) O(n_i^2) O(ni2) 时间内计算出关于标签 i i i 的路径数,则总时间复杂度为 O ( ∑ n i 2 ) O(\sum n_i^2) O(∑ni2)。当所有格子标签相同时,得到最坏时间复杂度 O ( N 4 ) O(N^4) O(N4),此方法会超时
方法二:对于固定的标签 k k k,设 d p i , j dp_{i,j} dpi,j 为从某个标签 k k k 的格子出发,停留在 ( i , j ) (i,j) (i,j) 的路径数。此方法都对于固定的标签,时间复杂度为 O ( N 2 ) O(N^2) O(N2),当标签数为 N 2 N^2 N2 时,得到最坏时间复杂度 O ( N 4 ) O(N^4) O(N4),会超时。
正解是将两个方法结合起来,当前考虑的标签数量少于 N N N 时,采用方法一,否则采用方法二,则可以得到时间复杂度为 O ( N 3 ) O(N^3) O(N3)。
证明如下:
对于方法一: ∑ n i 2 ≤ N ∑ n i ≤ N 3 \sum n_i^2\le N\sum n_i\le N^3 ∑ni2≤N∑ni≤N3
方法二:每个标签 O ( N 2 ) O(N^2) O(N2),但使用方法二的标签数量不多于 N N N,故时间复杂度 O ( N 3 ) O(N^3) O(N3)
代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <array>
#include <numeric>
#include <functional>
#include <assert.h>
using i64 = long long;
const int P = 998244353;
int fast_pow(int x, int y) {
int ret = 1;
for (; y; x = 1LL * x * x % P, y >>= 1)
if (y & 1) ret = 1LL * ret * x % P;
return ret;
}
int C_calc(int n, int m, const std::vector<int>& fac) {
return 1LL * fac[n] * fast_pow(fac[m], P - 2) % P * fast_pow(fac[n - m], P - 2) % P;
}
void solve() {
std::vector<int> fac(400 * 2);
fac[0] = 1;
for (int i = 1; i < 400 * 2; i++) {
fac[i] = 1LL * fac[i - 1] * i % P;
}
std::vector<std::vector<int>> C(800, std::vector<int>(400));
for (int i = 0; i < 800; i++) {
for (int j = 0; j < 400; j++) {
if (i >= j)
C[i][j] = C_calc(i, j, fac);
else
C[i][j] = 0;
}
}
int ans = 0;
int n;
std::cin >> n;
std::vector<std::vector<int>> a(n, std::vector<int>(n));
std::vector<std::vector<std::pair<int, int>>> pos(n * n, std::vector<std::pair<int, int>>());
std::vector<int> num(n * n);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
std::cin >> a[i][j];
a[i][j]--;
num[a[i][j]]++;
pos[a[i][j]].emplace_back(i, j);
}
}
for (int i = 0; i < n * n; i++) {
if (num[i] <= 0) continue;
if (num[i] < n) {
for (int j = 0; j < pos[i].size(); j++) {
for (int k = j; k < pos[i].size(); k++) {
int x1 = pos[i][j].first, y1 = pos[i][j].second;
int x2 = pos[i][k].first, y2 = pos[i][k].second;
if (x2 >= x1 && y2 >= y1)
ans = (ans + C[y2 - y1 + x2 - x1][x2 - x1]) % P;
}
}
}
else {
std::vector<std::vector<int>> dp(n, std::vector<int>(n, 0));
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
dp[j][k] = (j - 1 >= 0 ? dp[j - 1][k] : 0) + (k - 1 >= 0 ? dp[j][k - 1] : 0) + (a[j][k] == i);
dp[j][k] %= P;
if (a[j][k] == i)
ans = (ans + dp[j][k]) % P;
}
}
}
}
std::cout << ans;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
solve();
return 0;
}