题意:给定两个二进制数 x , y x,y x,y,每次可以选择 x x x 二进制表达中的其中一位 b b b,然后执行 x ← x − b x \leftarrow x-b x←x−b 或 x ← x + b x \leftarrow x+b x←x+b。问 x x x 最少经过多少次操作变成 y y y。 1 ≤ x , y ≤ 1 0 9 1 \le x,y \le 10^9 1≤x,y≤109。
解法:只要数字不为 0 0 0,就一直存在 b = 1 b=1 b=1,因而首先特判 x = y x=y x=y 的情况,再判断 x x x 是否为 0 0 0,若不为 0 0 0 则输出 ∣ x − y ∣ |x-y| ∣x−y∣。
题意:给定 n n n 表示有一个由 { 1 , 2 , 3 , ⋯ , 2 n } \{1,2,3,\cdots,2n\} {1,2,3,⋯,2n} 构成的 2 n 2n 2n 张牌的初始牌堆,同时自己这里有一个空的牌堆。执行以下的操作:
问对于 ( 2 n ) ! (2n)! (2n)! 种牌的排列,总的抽取牌数有多少,对特定数字 m m m 取模。多测, ∑ n ≤ 300 \sum n \le 300 ∑n≤300, 1 ≤ m ≤ 1 0 9 1\le m \le 10^9 1≤m≤109。
解法:首先不考虑空间复杂度,定义 f i , x , y , k f_{i,x,y,k} fi,x,y,k 表示当前在抽取第 2 n − x − y + 1 2n-x-y+1 2n−x−y+1 张牌时,小于等于 n n n 的牌张数还剩 x x x 张,大于 n n n 的牌数还剩 y y y 张,在抽第 2 n − x − y 2n-x-y 2n−x−y 张(上一轮)牌时(1)如果是小于等于 n n n 的牌,则选了这小于等于 n n n 的剩下来的 x x x 张牌里面第 k k k 小的牌;(2)如果是大于 n n n 的牌,则选了这大于 n n n 的剩下来的 y y y 张牌里面第 k k k 小的牌;(该状态用 i ∈ { 0 , 1 } i \in \{0,1\} i∈{0,1} 区分)作为的现有方案总数(不考察后续初始牌堆的牌顺序)。
这样构造方案的思路:首先比较容易想到的是,维护大于 n n n 和小于等于 n n n 的个数,然后维护一个状态表示上一次是抽到大于的还是小于等于的。但是这样在枚举当前轮的时候,就无法避免重复选择的问题。因而修正到去掉已经选过的之后排多少,这样操作就不会导致重复问题,并且也方便刻画当前的大小。
既然不考虑重复问题,那么可以写出下面的转移方程:
{ f 0 , x , y , k ← ∑ i = 1 k f 0 , x + 1 , y , i + ∑ i = 0 y + 1 f 1 , x , y + 1 , i f 1 , x , y , k ← ∑ i = k + 1 x + 1 f 1 , x + 1 , y , i + ∑ i = 0 y + 1 f 0 , x , y + 1 , i \begin{cases} \displaystyle f_{0,x,y,k} \leftarrow \sum_{i=1}^{k}f_{0,x+1,y,i}+\sum_{i=0}^{y+1}f_{1,x,y+1,i}\\ \displaystyle f_{1,x,y,k} \leftarrow \sum_{i=k+1}^{x+1}f_{1,x+1,y,i}+\sum_{i=0}^{y+1}f_{0,x,y+1,i}\\ \end{cases} ⎩ ⎨ ⎧f0,x,y,k←i=1∑kf0,x+1,y,i+i=0∑y+1f1,x,y+1,if1,x,y,k←i=k+1∑x+1f1,x+1,y,i+i=0∑y+1f0,x,y+1,i
对于答案统计,可以考虑对每一步成功抽牌进行分步统计——即不再统计有多少种抽牌方式能抽到 i i i 张牌,而是在能抽到第 i i i 张牌的状态中,每次叠加它的次数(贡献)。
不难发现状态数有 O ( n 3 ) O(n^3) O(n3) 个,对于单个状态的计算如果按照上式直接计算,会达到 O ( n 4 ) O(n^4) O(n4) 的复杂度。不难注意到第四个维度可以前缀和,因而可以使用前缀和优化掉第四维,得到下面一个未经过空间优化的代码(会 MLE)。
注意下面的代码中,为了统一 dp 方程形式,在 [ 1 , n ] [1,n] [1,n] 部分是维护的第 k k k 大,在 [ n + 1 , 2 n ] [n+1,2n] [n+1,2n] 部分维护的是第 k k k 小。
#include
using namespace std;
#define int long long
int dp[2][310][310][310];
int solve()
{
int n, mod;
cin >> n >> mod;
// A:阶乘 C:组合数
vector<int> fac(n * 2 + 3);
vector<vector<int>> C(n * 2 + 3, vector<int>(n * 2 + 3));
C[0][0] = fac[0] = 1;
for (int i = 1; i <= n * 2; i++)
{
fac[i] = fac[i - 1] * i % mod;
C[i][0] = 1;
for (int j = 1; j <= i; j++)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
}
// 清空
for (int i = 0; i <= n + 3; i++)
for (int j = 0; j <= n + 3; j++)
for (int k = 0; k <= n + 3; k++)
dp[0][i][j][k] = dp[1][i][j][k] = 0;
// 快速取模
function<void(int &, int)> add = [&](int &x, int y)
{
if ((x += y) >= mod)
x -= mod;
};
function<void(int &, int)> del = [&](int &x, int y)
{
if ((x -= y) < 0)
x += mod;
};
// 任何情况,都能拿走一张牌
int ans = fac[2 * n];
// 初始条件:在还没开始抽牌之前,大([n+1,2n])还剩n张,小([1,n])也剩n张。
for (int i = 1; i <= n; i++)
{
dp[0][n][n][i] = i % mod; // 一开始是小,当前选择的牌是[1,i]范围,有i种
dp[1][n][n][i] = i % mod; // 一开始是大,同理
}
// 枚举大小两类牌在本轮抽取完成后,各剩多少(x,y),倒序枚举
for (int x = n; x >= 0; x--)
{
for (int y = n; y >= 0; y--)
{
if (x + y >= 2 * n) // 初值设定过
continue;
// 本次抽的是[1,n]中的牌
for (int k = 1; k <= x; k++)
{
if (x != n) // 上一轮抽到一张范围在[1,n]的牌,如果要继续游戏,如果当前抽到k,则上一轮抽出来的牌必须在[1,k]的范围(比当前小)
add(dp[0][x][y][k], (dp[0][x + 1][y][x + 1] - dp[0][x + 1][y][k] + mod) % mod);
if (y != n) // 再枚举当前抽到的是一张大的,这时一定可以接着抽
add(dp[0][x][y][k], dp[1][x][y + 1][y + 1]);
// 能走到当前这一步的状态,都统计一下这一次抽牌对答案的贡献
add(ans, dp[0][x][y][k] * fac[x + y - 1] % mod);
}
// 前缀和一下
for (int k = 1; k <= x; k++)
add(dp[0][x][y][k], dp[0][x][y][k - 1]);
// 本次抽到的是[n+1,2n]中的牌
for (int k = 1; k <= y; k++)
{
if (y != n) // 上一轮抽到一张范围在[n+1,2n]的牌,如果要继续游戏,如果当前抽到k,则上一轮抽出来的牌必须在[k+1,y+1]的范围(比当前大)
add(dp[1][x][y][k], (dp[1][x][y + 1][y + 1] - dp[1][x][y + 1][k] + mod) % mod);
if (x != n) // 上一轮抽到一张[1,n]的牌,都可以继续抽牌
add(dp[1][x][y][k], dp[0][x + 1][y][x + 1]);
// 能走到当前这一步的状态,都统计一下这一次抽牌对答案的贡献
add(ans, dp[1][x][y][k] * fac[x + y - 1] % mod);
}
// 前缀和一下
for (int k = 1; k <= y; k++)
add(dp[1][x][y][k], dp[1][x][y][k - 1]);
}
}
// 全部能抽完的部分多算了一步
add(ans, (fac[2 * n] - dp[0][1][0][1] - dp[1][0][1][1] + mod) % mod);
return ans;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--)
cout << solve() << "\n";
return 0;
}
由于每次 x x x 只会从 x + 1 x+1 x+1 转移,因而可以考虑再滚动掉最外层的一维数组(即 x x x),即使用 0 , 1 0,1 0,1 两个状态来表示当前的 x x x 和 x + 1 x+1 x+1。
#include
using namespace std;
long long dp[2][2][310][310];
void Solve()
{
int n, mod;
scanf("%d%d", &n, &mod);
vector<long long> fac(n * 2 + 3);
vector<vector<long long>> C(n * 2 + 3, vector<long long>(n * 2 + 3));
C[0][0] = fac[0] = 1;
for (int i = 1; i <= n * 2; i++)
{
fac[i] = fac[i - 1] * i % mod;
C[i][0] = 1;
for (int j = 1; j <= i; j++)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
}
for (int i = 0; i <= 1; i++)
for (int j = 0; j <= n + 3; j++)
for (int k = 0; k <= n + 3; k++)
dp[0][i][j][k] = dp[1][i][j][k] = 0;
// 快速取模
function<void(long long &, long long)> add = [&](long long &x, long long y)
{
x += y;
if (x >= mod)
x -= mod;
};
function<void(long long &, long long)> del = [&](long long &x, long long y)
{
x -= y;
if (x < 0)
x += mod;
};
long long ans = fac[2 * n];
// 在下面的代码中,为方便编写,因而考虑用(n-x)的奇偶性来作为滚动数组的标识
// 初始条件
for (int i = 1; i <= n; i++)
{
dp[0][0][n][i] = i % mod;
dp[1][0][n][i] = i % mod;
}
int op = 0; // 滚动后x的代表
// 枚举两个各剩多少
for (int x = n; x >= 0; x--)
{
for (int y = n; y >= 0; y--)
{
if (x + y == 2 * n || x + y == 0)
continue;
// 第一类转移
for (int k = 1; k <= x; k++)
{
if (x != n)
add(dp[0][op][y][k], (dp[0][op ^ 1][y][x + 1] - dp[0][op ^ 1][y][k] + mod) % mod);
if (y != n)
add(dp[0][op][y][k], dp[1][op][y + 1][y + 1]);
add(ans, dp[0][op][y][k] * fac[x + y - 1] % mod);
}
for (int k = 1; k <= x; k++)
add(dp[0][op][y][k], dp[0][op][y][k - 1]);
// 第二类转移
for (int k = 1; k <= y; k++)
{
if (y != n)
add(dp[1][op][y][k], (dp[1][op][y + 1][y + 1] - dp[1][op][y + 1][k] + mod) % mod);
if (x != n)
add(dp[1][op][y][k], dp[0][op ^ 1][y][x + 1]);
add(ans, dp[1][op][y][k] * fac[x + y - 1] % mod);
}
for (long long k = 1; k <= y; k++)
add(dp[1][op][y][k], dp[1][op][y][k - 1]);
}
op ^= 1;
for (long long i = 0; i <= n + 3; i++)
for (long long j = 0; j <= n + 3; j++)
dp[0][op][i][j] = dp[1][op][i][j] = 0;
}
// 去掉抽走2n张牌的重复贡献
add(ans, (fac[2 * n] - dp[1][op ^ 1][1][1] * 2 + mod * 2) % mod);
printf("%lld\n", ans);
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
Solve();
return 0;
}
题意:给定一 n × n n\times n n×n 的 01 矩阵,每次可以翻转一行或一列,执行若干次操作。若将操作完成后的矩阵的每一行从做到右视为一个二进制数 { r } i = 1 n \{r\}_{i=1}^n {r}i=1n,每一列从上到下视为 { c } i = 1 n \{c\}_{i=1}^n {c}i=1n,要求 min ( r i ) ≥ max ( c i ) \min(r_i) \ge \max(c_i) min(ri)≥max(ci),问是否可以实现,可以实现求最小操作次数。 1 ≤ n ≤ 300 1 \le n \le 300 1≤n≤300。
解法:假设第一行有一个 1 1 1,则 max ( c i ) ≥ 2 n − 1 \max(c_i) \ge 2^{n-1} max(ci)≥2n−1,此时 min ( r i ) ≥ 2 n − 1 \min(r_i) \ge 2^{n-1} min(ri)≥2n−1,即第一列每个数字都是 1 1 1,则此时 max ( c i ) = 2 n − 1 \max(c_i)=2^n-1 max(ci)=2n−1,则要求整个矩阵都是 1 1 1。因而全 1 1 1 矩阵是合理的。
如果第一行全 0 0 0,则 min ( r i ) = 0 \min(r_i)=0 min(ri)=0,则 max ( c i ) = 0 \max(c_i)=0 max(ci)=0,整个矩阵全 0 0 0。
因而整个矩阵必须所有数字都相同。
考虑维护方程 { c i } \{c_i\} {ci} 和 { r j } \{r_j\} {rj}。如果最后是全 0 0 0,如果当前 a i , j = 1 a_{i,j}=1 ai,j=1,则 c i ≠ r j c_i \ne r_j ci=rj,反之相等。因而维护一个 01 种类并查集即可。
#include
using namespace std;
const int N = 2000;
char a[N + 5][N + 5];
// [1, n] row0; [n+1,2n] row1
// [2*n+1,3*n] col0
int father[4 * N + 5], n;
int getfather(int x)
{
return father[x] == x ? x : father[x] = getfather(father[x]);
}
int getid(int id, int flag, int col)
{
int ans = id;
if (flag)
ans += 2 * n;
if (col)
ans += n;
return ans;
}
void merge(int x, int y)
{
x = getfather(x);
y = getfather(y);
if (x != y)
father[x] = y;
}
bool check(int x, int y)
{
return getfather(x) == getfather(y);
}
int solve(int op)
{
int m = n * 2;
for (int i = 1; i <= 2 * m; i++)
father[i] = i;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (a[i][j] == ('1' ^ op))
{
merge(getid(i, 0, 0), getid(j, 1, 1));
merge(getid(i, 0, 1), getid(j, 1, 0));
}
else
{
merge(getid(i, 0, 0), getid(j, 1, 0));
merge(getid(i, 0, 1), getid(j, 1, 1));
}
for (int i = 1; i <= n; i++)
if (check(i, i + n))
return -1;
for (int i = 1; i <= n; i++)
if (check(i + 2 * n, i + 3 * n))
return -1;
map<int, int> siz;
for (int i = 1; i <= n; i++)
siz[getfather(i)]++;
for (int i = 2 * n + 1; i <= 3 * n; i++)
siz[getfather(i)]++;
int ans = 2 * n;
for (auto [x, y] : siz)
ans = min(ans, y);
return min(ans, 2 * n - ans);
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%s", a[i] + 1);
if (solve(0) == -1)
{
printf("-1");
return 0;
}
printf("%d", min(solve(0), solve(1)));
return 0;
}
题意:给定一张 G ( n , m ) G(n,m) G(n,m) 的有向图,使用 dfs 算法求解从 1 1 1 开始的单源最短路,问给定的图能否在任何边遍历顺序下都正确输出。 1 ≤ n , m ≤ 1 0 5 1 \le n,m\le 10^5 1≤n,m≤105。
解法:为什么我们要找支配树?可以考虑以下三个例子:
基本错误型:一个点( 2 2 2)有多种到达方式。
一个环:多次遍历到的点不一定是有重复的。
破环:多种遍历方式会导致破环方式不同(如 6 → 4 6 \to 4 6→4, 4 → 2 4 \to 2 4→2, 5 → 3 5 \to 3 5→3),从而影响答案。
首先我们依然可以建立一个 bfs 树,找到从 1 1 1 出发到每个点的最短距离。然后考虑原图中的每一条边:
No
。#include
using namespace std;
const int MAXN = 5e5 + 10;
namespace dtree
{
const int MAXN = 500020;
vector<int> E[MAXN], RE[MAXN], rdom[MAXN];
int S[MAXN], RS[MAXN], cs;
int par[MAXN], val[MAXN], sdom[MAXN], rp[MAXN], dom[MAXN];
void clear(int n)
{
cs = 0;
for (int i = 0; i <= n; i++)
{
par[i] = val[i] = sdom[i] = rp[i] = dom[i] = S[i] = RS[i] = 0;
E[i].clear();
RE[i].clear();
rdom[i].clear();
}
}
void add_edge(int x, int y) { E[x].push_back(y); }
void Union(int x, int y) { par[x] = y; }
int Find(int x, int c = 0)
{
if (par[x] == x)
return c ? -1 : x;
int p = Find(par[x], 1);
if (p == -1)
return c ? par[x] : val[x];
if (sdom[val[x]] > sdom[val[par[x]]])
val[x] = val[par[x]];
par[x] = p;
return c ? p : val[x];
}
void dfs(int x)
{
RS[S[x] = ++cs] = x;
par[cs] = sdom[cs] = val[cs] = cs;
for (int e : E[x])
{
if (S[e] == 0)
dfs(e), rp[S[e]] = S[x];
RE[S[e]].push_back(S[x]);
}
}
int solve(int s, int *up)
{
dfs(s);
for (int i = cs; i; i--)
{
for (int e : RE[i])
sdom[i] = min(sdom[i], sdom[Find(e)]);
if (i > 1)
rdom[sdom[i]].push_back(i);
for (int e : rdom[i])
{
int p = Find(e);
if (sdom[p] == i)
dom[e] = i;
else
dom[e] = p;
}
if (i > 1)
Union(i, rp[i]);
}
for (int i = 2; i <= cs; i++)
if (sdom[i] != dom[i])
dom[i] = dom[dom[i]];
for (int i = 2; i <= cs; i++)
up[RS[i]] = RS[dom[i]];
return cs;
}
}
int up[MAXN];
vector<int> G[MAXN];
int dep[MAXN], f[21][MAXN];
void dfs(int x, int fa)
{
dep[x] = dep[fa] + 1;
for (int i = 0; i <= 19; i++)
f[i + 1][x] = f[i][f[i][x]];
for (auto it : G[x])
{
if (it == fa)
continue;
f[0][it] = x;
dfs(it, x);
}
}
int lca(int x, int y)
{
if (dep[x] < dep[y])
swap(x, y);
for (int i = 20; i >= 0; i--)
{
if (dep[f[i][x]] >= dep[y])
x = f[i][x];
if (x == y)
return x;
}
for (int i = 20; i >= 0; i--)
if (f[i][x] != f[i][y])
x = f[i][x], y = f[i][y];
return f[0][x];
}
string solve()
{
int n, m;
cin >> n >> m;
dtree::clear(n);
for (int i = 1; i <= n; i++)
G[i].clear();
for (int i = 1; i <= m; i++)
{
int x, y;
cin >> x >> y;
dtree::E[x].push_back(y);
}
dtree::solve(1, up);
for (int i = 2; i <= n; i++)
G[up[i]].push_back(i);
dfs(1, 0);
const int inf = 1e9;
vector<int> dis(n + 1, inf);
dis[1] = 0;
queue<int> q;
q.push(1);
bool flag = true;
while (!q.empty())
{
int x = q.front();
q.pop();
for (auto it : dtree::E[x])
if (dis[it] == inf)
{
q.push(it);
dis[it] = dis[x] + 1;
continue;
}
else if (dis[it] != dis[x] + 1)
{
if (lca(it, x) != it)
flag = false;
}
}
if (flag)
return "Yes";
else
return "No";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--)
cout << solve() << "\n";
return 0;
}
题意:给定两个十进制数 x , y x,y x,y,每次可以选择 x x x 十进制表达中的其中一位 b b b,然后执行 x ← x − b x \leftarrow x-b x←x−b 或 x ← x + b x \leftarrow x+b x←x+b。问 x x x 最少经过多少次操作变成 y y y。多次询问, 1 ≤ q ≤ 3 × 1 0 5 1 \le q \le 3\times 10^5 1≤q≤3×105, 1 ≤ x , y ≤ 3 × 1 0 5 1 \le x,y \le 3\times 10^5 1≤x,y≤3×105,强制在线。
解法:显然,每个点可以向外连出若干条边模拟一次操作。如果数字范围足够小那么是一个简单的全源最短路问题,但是本题数据范围较大,但是我们仍然需要这一建图的思想。
考虑固定枚举出中间某个特定的变化步骤,即取出一段长度为 9 9 9 的一段——因为一步操作至多使得 x x x 变化 9 9 9。然后采用分治的思想:
定义起始数字 x x x 和结束数字 y y y 以及中间变化过程都在区间 [ l , r ] [l,r] [l,r],考虑中间的 9 9 9 个数字 [ m , m + 8 ] [m,m+8] [m,m+8]。这时可以考虑以 [ m , m + 8 ] [m,m+8] [m,m+8] 依次每个点跑单源最短路(bfs),求得每个点到 [ m , m + 8 ] [m,m+8] [m,m+8] 中每个点到其他点的正向(即 x → [ m , m + 8 ] x \to [m,m+8] x→[m,m+8])和反向(即 [ m , m + 8 ] → x [m,m+8] \to x [m,m+8]→x)的距离。这时这个图的大小和源都相对较少,可以承受。
如果起始数字和结束数字分列在 [ l , m ) [l,m) [l,m) 和 ( m + 8 , r ] (m+8,r] (m+8,r] 区间,则必然需要在变化过程中经过 [ m , m + 8 ] [m,m+8] [m,m+8]。那么答案可以是 x → [ m , m + 8 ] x \to [m,m+8] x→[m,m+8] 的正向距离,加上 y → [ m , m + 8 ] y \to [m,m+8] y→[m,m+8] 的反向距离之和。
如果都分在同一侧,则递归在两个子区间进行分治处理,预处理记录每个点在 O ( log n ) O(\log n) O(logn) 层中到枢纽点 [ m , m + 8 ] [m,m+8] [m,m+8] 的两个距离。
考虑一次查询操作,则需要对每一个包含 [ l , r ] [l,r] [l,r] 的区间都做一次答案更新操作: x → [ m , m + 8 ] x \to [m,m+8] x→[m,m+8] 的正向距离,加上 y → [ m , m + 8 ] y \to [m,m+8] y→[m,m+8] 的反向距离之和。这样单次查询操作仅需要查询 O ( log n ) O(\log n) O(logn) 个区间,复杂度就是 O ( k log n ) O(k \log n) O(klogn)。整体复杂度 O ( k ( n + q ) log n ) O(k(n+q)\log n) O(k(n+q)logn)。
题意:给定一个 n × m n\times m n×m 的字符矩阵 { S } ( i , j ) = ( 1 , 1 ) ( n , m ) \{S\}_{(i,j)=(1,1)}^{(n,m)} {S}(i,j)=(1,1)(n,m),定义一个 n × n n\times n n×n 的子矩阵是优美的当且仅当 ∀ i , j ∈ [ 1 , n ] \forall i,j \in [1,n] ∀i,j∈[1,n], S i , j = S n − i + 1 , n − j + 1 S_{i,j}=S_{n-i+1,n-j+1} Si,j=Sn−i+1,n−j+1。问 { S } \{S\} {S} 中有多少个优美的子矩阵。 1 ≤ n , m ≤ 2 × 1 0 3 1\le n,m \le 2\times 10^3 1≤n,m≤2×103。
解法:本题卡常,请使用 O ( n 2 ) \mathcal O(n^2) O(n2) 的做法通过。
首先枚举哪一行是中央对称行。当固定中央行之后,考虑在这一行做 Manacher。传统的 Manacher 是仅单字符匹配成立即可扩展回文半径,在本题这种矩阵对称中,可以考虑对一个矩阵区域(正向绿色等于反向紫色)的匹配作为扩展条件:
因而使用二维哈希判断子矩阵对称相等,结合 Manacher 算法即可通过。复杂度同每行 Manacher 算法,即 O ( n m ) O(nm) O(nm)。
本题严重卡常。
题意:给定一个长度为 n n n 的序列 { a } i = 1 n \{a\}_{i=1}^n {a}i=1n,一次操作可以选择两个不同的数字 a i a_i ai 和 a j a_j aj 执行 a i ← a i + 1 , a j ← a j − 1 a_i \leftarrow a_i+1,a_j \leftarrow a_j-1 ai←ai+1,aj←aj−1。问能不能有限次操作内让序列中每个数字都是质数。 1 ≤ n ≤ 1 0 3 1 \le n \le 10^3 1≤n≤103, 1 ≤ a i ≤ 1 0 9 1 \le a_i\le 10^9 1≤ai≤109。
解法:当 n = 1 n=1 n=1 时,显然和 s s s 为质数才行。
当 n ≥ 2 n \ge 2 n≥2 时, s ≥ 2 n s \ge 2n s≥2n(每个数字都是最小的质数 2 2 2)。
当 n = 2 n=2 n=2 时,如果 s s s 为偶数,则由哥德巴赫猜想一定成立。如果 s s s 为奇数,则必然是 2 + ( n − 2 ) 2+(n-2) 2+(n−2)。检查 n − 2 n-2 n−2 是否为质数即可。
当 n ≥ 3 n \ge 3 n≥3 时,可以首先先安排 n − 2 n-2 n−2 个 2 2 2,问题退化到 n = 2 n=2 n=2 的情形。当 s − 2 n s-2n s−2n 为偶数时,哥德巴赫猜想使得一定有解。如果 s − 2 n s-2n s−2n 是奇数,则可以让前面 n − 2 n-2 n−2 个 2 2 2 中其中一个 2 2 2 变成 3 3 3,则此时 s − 2 n − 1 s-2n-1 s−2n−1 为偶数,如果 s − 2 n − 1 ≥ 4 s-2n-1 \ge 4 s−2n−1≥4,则同样利用哥德巴赫猜想可以证明成立。
题意:有一个 n n n 个点的树,第 i i i 个点拥有的颜色种类由二进制数 c i c_i ci 定义。 q q q 次询问,每次从 u u u 出发到 v v v,初始自选颜色,一秒的时间里可以移动到相邻同色节点(即该节点有自身的一种颜色),或者在一个点变换自身颜色(必须是这个点已经有的颜色)。问花费的最少时间。 1 ≤ n , q ≤ 5 × 1 0 5 1 \le n,q \le 5\times 10^5 1≤n,q≤5×105, 0 ≤ c i < 2 60 0 \le c_i < 2^{60} 0≤ci<260。
解法:一个贪心的想法是,能尽可能维持当前颜色不变就尽可能不变。因而首先处理出从当前节点 u u u 向上最多能不变色走到哪里,然后再倍增处理出变 2 k 2^k 2k 次颜色会跳到哪里(因为可能不变色一次只能走一条边)。
考虑 u → v u \to v u→v 可以拆分成 u → l c a ( u , v ) → v u \to {\rm lca}(u,v) \to v u→lca(u,v)→v,因而拆分成祖孙链上的问题。对于单程 u → l c a ( u , v ) u \to {\rm lca}(u,v) u→lca(u,v),首先倍增跳到最靠近 l c a ( u , v ) {\rm lca}(u,v) lca(u,v) 的变色点 w w w,然后再维护不变色段 w → l c a ( u , v ) w \to {\rm lca}(u,v) w→lca(u,v) 的可行颜色,对 v v v 做同样的考虑。两侧取交集非空则不需要在转折点变色。
#include
using namespace std;
const int maxn = 5e5 + 10;
vector<int> ve[maxn];
int dep[maxn], f[21][maxn];
int jmp[21][maxn];
int col[60][maxn]; // 对于每个颜色能跳的最高的节点
long long a[maxn];
int Fa[maxn];
void dfs(int x, int fa)
{
dep[x] = dep[fa] + 1;
Fa[x] = fa;
int up = -1;
for (int i = 0; i < 60; i++)
{
col[i][x] = col[i][fa];
if ((a[x] >> i & 1) && col[i][x] == -1)
col[i][x] = x;
if (~a[x] >> i & 1)
col[i][x] = -1;
if (up == -1 || (col[i][x] != -1 && dep[up] > dep[col[i][x]]))
up = col[i][x];
}
jmp[0][x] = up;
for (int i = 0; i <= 19; i++)
{
f[i + 1][x] = f[i][f[i][x]];
if (jmp[i][x] != -1)
jmp[i + 1][x] = jmp[i][jmp[i][x]];
}
for (auto it : ve[x])
{
if (it == fa)
continue;
f[0][it] = x;
dfs(it, x);
}
}
int lca(int x, int y)
{
if (dep[x] < dep[y])
swap(x, y);
for (int i = 20; i >= 0; i--)
{
if (dep[f[i][x]] >= dep[y])
x = f[i][x];
if (x == y)
return x;
}
for (int i = 20; i >= 0; i--)
if (f[i][x] != f[i][y])
x = f[i][x], y = f[i][y];
return f[0][x];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, q;
cin >> n >> q;
memset(col, -1, sizeof(col));
memset(jmp, -1, sizeof(jmp));
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i < n; i++)
{
int x, y;
cin >> x >> y;
ve[x].push_back(y);
ve[y].push_back(x);
}
dfs(1, 0);
function<int(int, int)> solve = [&](int x, int y)
{
int L = lca(x, y);
int ans = dep[x] + dep[y] - dep[L] * 2;
if (x == L)
swap(x, y);
if (x == L)
return 0;
if (y == L)
{
for (int i = 20; i >= 0; i--)
if (jmp[i][x] != -1 && dep[jmp[i][x]] > dep[L])
{
ans += 1 << i;
x = jmp[i][x];
}
if (a[x] & a[Fa[x]])
return ans;
return -1;
}
for (int i = 20; i >= 0; i--)
if (jmp[i][x] != -1 && dep[jmp[i][x]] > dep[L])
{
ans += 1 << i;
x = jmp[i][x];
}
for (int i = 20; i >= 0; i--)
if (jmp[i][y] != -1 && dep[jmp[i][y]] > dep[L])
{
ans += 1 << i;
y = jmp[i][y];
}
if (jmp[0][x] == -1 || jmp[0][y] == -1)
return -1;
if (dep[jmp[0][x]] > dep[L] || dep[jmp[0][y]] > dep[L])
return -1;
bool flag = false;
for (int i = 0; i < 60; i++)
if (col[i][x] != -1 && col[i][y] != -1)
{
if (dep[col[i][x]] <= dep[L] && dep[col[i][y]] <= dep[L])
flag = true;
}
if (flag)
return ans;
else
return ans + 1;
};
while (q--)
{
int x, y;
cin >> x >> y;
cout << solve(x, y) << "\n";
}
return 0;
}
题意:给定 n n n 个点和 m m m 对偏序关系 ⟨ u , v ⟩ \langle u,v\rangle ⟨u,v⟩,构造最少的排列数目 k k k,使得在这 k k k 个排列中至少有一个排列满足 u < v u
解法:数据范围过于庞大意味着 k k k 必然不大。其实很容易发现一种任意情况下都成立的方案: k = 2 k=2 k=2,一个排列是 { 1 , 2 , 3 , ⋯ , n } \{1,2,3,\cdots,n\} {1,2,3,⋯,n},另一个排列是 { n , n − 1 , n − 2 , ⋯ , 1 } \{n,n-1,n-2,\cdots,1\} {n,n−1,n−2,⋯,1}。因为 u < v u
考虑什么时候满足 k = 1 k=1 k=1。如果 k = 1 k=1 k=1,则必然存在一个拓扑序列。考虑依照 u → v u \rightarrow v u→v 建图,即必须访问完 u u u 才能访问 v v v。如果该 DAG 有拓扑序,那么这个排列就是合法的。否则则按 k = 2 k=2 k=2 的方案构造。
#include
using namespace std;
const int N = 1000000;
int in[N + 5];
vector<int> G[N + 5];
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; i++)
{
scanf("%d%d", &u, &v);
G[u].push_back(v);
in[v]++;
}
queue<int> q;
for (int i = 1; i <= n; i++)
if (!in[i])
q.push(i);
vector<int> ans;
while (!q.empty())
{
int tp = q.front();
q.pop();
ans.push_back(tp);
for (auto x : G[tp])
{
in[x]--;
if (!in[x])
q.push(x);
}
}
if (ans.size() == n)
{
printf("1\n");
for (auto x : ans)
printf("%d ", x);
}
else
{
printf("2\n");
for (int i = 1; i <= n; i++)
printf("%d ", i);
printf("\n");
for (int i = n; i >= 1; i--)
printf("%d ", i);
printf("\n");
}
return 0;
}