2020 ICPC Xiaomi (Qualification Round 1) - Part Solutions

Link: https://ac.nowcoder.com/acm/contest/7501

A.Intelligent Warehouse

题意

给定一个含有 $n$ 个数的序列 $a_1, a_2, …,a_n$,从中选出尽可能多的数,使得选出的数中,对于任意两个数 $a_i, a_j$,要么 $a_i$ 是 $a_j$ 的倍数,要么 $a_j$ 是 $a_i$ 的倍数。问最多能选出多少个数满足条件。

思路

首先统计序列中每个数 $a_i$ 出现的次数记为 $cnt[a[i]]$。

注意到对于任意两个数 $i, j$ ($i < j$,且 $j$ 是 $i$ 的倍数),如果我们已知选择 $i$ 的条件下,从原序列中最多可以选出 $f[i]$ 个落在区间 $[1, i]$ 的数(一个数可以选择多次)满足条件;

那么选择 $j$ 时,从原序列中就最多可以选出 $f[j] = max(f[j], f[i] + cnt[j])$ 个落在区间 $[1, j]$ 的数满足条件。这个递推式子显然是无后效的。

所以对于 $[1, max(a_i)]$ 中的每个数 $i$,使用上式进行递推即可。递推 $i$ 时如果枚举 $i$ 的所有倍数会超时,我们只需要枚举 $i$ 不超过原序列最大值的素数倍数即可,这是因为任意合数一定能表示成若干个比它小的质数的积,因此每个合数必然是另一个数的质数倍。

时间复杂度 $O(V\log K)$,$V$ 为 $a_i$ 的最大取值,$K$ 为 1e7 内的质数个数。

代码

写法一

#include <bits/stdc++.h> 
using namespace std;

const int maxn = 200050, maxv = 1e7 + 10, sqrtv = 1e4;

int primes[maxv], cnt[maxv], dp[maxv], n, tot;
bool np[maxv];

template<class T>void read(T &x) {
	T a = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
		f = ch == '-' ? -1 : f, ch = getchar();
	while (ch >= '0' && ch <= '9')
		a = a * 10 + ch - '0', ch = getchar();
	x = a * f;
}

void sieve() {
	np[0] = np[1] = 1;
	for (int i = 2; i < maxv; i++) {
		if (!np[i])
			primes[tot++] = i;
		for (int j = 0; j < tot && i * primes[j] < maxv; j++) {
			np[i * primes[j]] = 1;
			if (i % primes[j] == 0)
				break;
		}
	}
}

int main() {
	sieve(), read(n);
	int lim = 0, ans = 1;
	for (int i = 0, x; i < n; i++)
		read(x), lim = max(x, lim), cnt[x]++;
	memset(dp, 0, sizeof dp);
	for (int i = 1; i <= lim; i++) {
		dp[i] += cnt[i];
		ans = max(ans, dp[i]);
		for (int j = 0; j < tot && i * primes[j] <= lim; j++)
			dp[i * primes[j]] = max(dp[i * primes[j]], dp[i]);
	}
	printf("%d", ans);
	return 0;
}

写法二

来自队友,似乎是换了一种方式实现,看不懂,总之贴上就是了。

#include<bits/stdc++.h>
#define N 200010
#define M 10000010
#define ll long long
using namespace std;
inline int read()
{
    int x=0,f=1; char c=getchar();
    while (c<'0' ||c>'9') {if (c=='-') f=-1; c=getchar();}
    while (c>='0' && c<='9') x=x*10+c-'0',c=getchar();
    return x*f;
}
int n;
int mx[M],f[N];
int prime[1000010],cnt=0;
vector<int>fj;
ll a[N];
int main()
{
    for(int i = 2; i <= 1e4; i++) {
        bool flag = 0;
        for (int j = 2; j * j <= i; j++)
            if (i % j == 0) {
                flag = 1;
                break;
            }
        if (!flag)
			prime[++cnt] = i;
    }
    n = read();
    for (int i = 1; i <= n; i++)
		a[i] = read();
    sort(a+1, a+1+n);
    int ans = 0;
    for (int i = 1;i <= n; i++) {
        int ai = a[i];
        f[i] = max(1, mx[1] + 1); 
		fj.clear();
        fj.push_back(1);
        for (int j = 1; j <= cnt; j++) {
            if (prime[j] > a[i])
				break;
            if (a[i] % prime[j] == 0) {
                ll p = prime[j];
                int len = fj.size();
                while (a[i] % prime[j] == 0) {
                    a[i] /= prime[j];
                    for (int k = 0; k < len; k++) {
                        f[i] = max(f[i], mx[fj[k] * p] + 1);
                        fj.push_back(fj[k] * p);
                    }
                    p *= prime[j];
                }
            }
        }
        if (a[i] > 1)
			f[i] = max(f[i], mx[ai] + 1);
        mx[ai] = f[i], ans = max(ans, f[i]);
    }
    printf("%d\n",ans);
    return 0;
}

B. Intelligent Robot

题意

$n \times m$ 的矩形迷宫中有若干堵墙,用起点为 $(x_{i1}, y_{i1})$,终点为 $(x_{i2}, y_{i2})$ 的线段表示。机器人要从起点 $(x_1, y_1)$ 走到终点 $(x_2, y_2)$,途中可以贴着墙走或碰到墙的边界,但不能够穿过墙体;求从起点到终点的最短路径。

思路

枚举任意两个关键点(关键点即线段的两端点、起点、终点),考虑这两个点是否可以连边:两个点能连边的条件是,这两个点连成的线段,不与任何一条给定的线段规范相交(两条线段规范相交:即两条线段只有一个交点且不为端点),如果两个关键点连成的线段不和其它给定的 $n$ 条线段相交,则将这两个点连一条权值为两点间距的双向边;建完图之后跑 Dijkstra 求最短路即可。

不要忘记检查起点和终点是否可以直接连接;以及每堵墙的两个端点也可以直接连一条权值为两点间距的边。

代码

#include <bits/stdc++.h>
using namespace std;
 
const int maxn = 350;
const double eps = 1e-8;
const double inf = (double)(1ll << 60);
 
inline int dcmp(double x) {
    if (fabs(x) <= eps)
        return 0;
    return x < 0 ? -1 : 1;
}
 
struct Point {
    double x, y;
    Point (double _x = 0, double _y = 0): x(_x), y(_y) {}
    Point operator + (const Point &b) const {
        return Point(x + b.x, y + b.y);
    }
    Point operator - (const Point &b) const {
        return Point(x - b.x, y - b.y);
    }
    Point operator * (const double c) const {
        return Point(x * c, y * c);
    }
    double operator * (const Point &b) const {
        return x * b.x + y * b.y;
    }
    double operator ^ (const Point &b) const {
        return x * b.y - y * b.x;
    }
    bool operator == (const Point &b) const {
        return dcmp(x - b.x) == 0 && dcmp(y - b.y) == 0;
    }
    bool operator < (const Point &b) const {
        return dcmp(x - b.x) < 0 || (dcmp(x - b.x) == 0 && dcmp(y - b.y) < 0);
    }
};
 
struct Wall {
    Point a, b;
    Wall() {}
    Wall(Point _a, Point _b): a(_a), b(_b) {}
} w[maxn];
 
int head[maxn << 1], cnt = 1;
struct Edge {
    int u, v, next;
    double w;
    Edge(int _u = 0, int _v = 0, int _n = 0, double _l = 0): u(_u), v(_v), next(_n), w(_l) {}
} e[maxn * maxn * 8];
 
typedef Point Vector;
 
#define p1(x) x*2
#define p2(x) x*2+1
 
bool segmentIntersect(Point a1, Point b1, Point a2, Point b2) {
    double c1 = (b1 - a1) ^ (a2 - a1), c2 = (b1 - a1) ^ (b2 - a1),
           c3 = (b2 - a2) ^ (a1 - a2), c4 = (b2 - a2) ^ (b1 - a2);
    return dcmp(c1) * dcmp(c2) < 0 && dcmp(c3) * dcmp(c4) < 0;
}
 
double len(Point x, Point y) {
    return sqrt((y-x) * (y-x));
}
 
void add_edge(int u, int v, double len) {
    e[cnt] = Edge(u, v, head[u], len);
    head[u] = cnt++;
    e[cnt] = Edge(v, u, head[v], len);
    head[v] = cnt++;
}
 
bool vis[maxn << 1];
double dist[maxn << 1];
 
double dijkstra(int n, int s, int t) {
    priority_queue<pair<double, int> > pq;
    memset(vis, 0, sizeof vis);
    fill(dist, dist + n, inf);
    dist[s] = 0.00, pq.push(make_pair(0.00, s));
    while (pq.size()) {
        int cur = pq.top().second;
        pq.pop();
        if (vis[cur])
            continue;
        vis[cur] = 1;
        for (int i = head[cur], y; i; i = e[i].next) {
            y = e[i].v;
            if (dist[y] > dist[cur] + e[i].w) {
                dist[y] = dist[cur] + e[i].w;
                pq.push(make_pair(-dist[y], y));
            }
        }
    }
    return dist[t];
}
 
int main() {
    int n, m, l;
    double x1, y1, x2, y2;
    scanf("%d%d%d", &n, &m, &l);
    for (int i = 0; i < l + 1; i++) {
        scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
        w[i] = Wall(Point(x1, y1), Point(x2, y2));
    }
    for (int i = 0; i < l + 1; i++) {
        for (int j = 0; j < l; j++) {
            if (i == j)
                continue;
            bool f1 = false, f2 = false, f3 = false, f4 = false;
            for (int k = 0; k < l; k++) {
                if (i == k || j == k)
                    continue;
                if (segmentIntersect(w[i].a, w[j].a, w[k].a, w[k].b))
                    f1 = 1;
                if (segmentIntersect(w[i].a, w[j].b, w[k].a, w[k].b))
                    f2 = 1;
                if (segmentIntersect(w[i].b, w[j].a, w[k].a, w[k].b))
                    f3 = 1;
                if (segmentIntersect(w[i].b, w[j].b, w[k].a, w[k].b))
                    f4 = 1;
            }
            if (!f1) add_edge(p1(i), p1(j), len(w[i].a, w[j].a));
            if (!f2) add_edge(p1(i), p2(j), len(w[i].a, w[j].b));
            if (!f3) add_edge(p2(i), p1(j), len(w[i].b, w[j].a));
            if (!f4) add_edge(p2(i), p2(j), len(w[i].b, w[j].b));
        }
        if (i != l)
            add_edge(p1(i), p2(i), len(w[i].a, w[i].b));
    }
    bool flag = false;
    for (int i = 0; i < l; i++) {
        if (segmentIntersect(w[i].a, w[i].b, w[l].a, w[l].b)) {
            flag = true;
            break;
        }
    }
    if (!flag)
        add_edge(p1(l), p2(l), len(w[l].a, w[l].b));
    double ans = dijkstra((l + 1) * 2, p1(l), p2(l));
    printf("%.4lf", ans);
     
    return 0;
}

C. Smart Browser

题意

给一串小写字母组成的字符串,计算形状 /\w 和连续的 w 之间出现的次数。

思路

签到。一个 w 会贡献 1 个答案,两个 w 一起出现会多贡献一个答案,所以每次遇到 w 时判断它的前一个是不是 w,如果是答案 +2,否则答案 +1。

代码

#include<bits/stdc++.h>
using namespace std;
char str[100010]; 
int main() {
	scanf("%s", str);
	int len = strlen(str);
	int ans = 0;
	for (int i = 0; i < len; i++) {
		if (str[i] == 'w') {
			if (i == 0)
                ans=1;
			else {
				ans++;
				if (str[i-1] == 'w')
                    ans++;	
			}
		}
	}
	cout<< ans <<endl;
} 

D. Router Mesh

题意

给定一个 $n$ 个点,$m$ 条边的无向图,分别计算删除编号为 $1, 2, …, n$ 的点时,图中还剩下多少个连通块。

思路

要解决这道题需要对 Tarjan 有一定的理解。

考虑使用 Tarjan 求无向图的割点时,$low[i]$ 的意义为:从节点 $i$ 及其 DFS 子树出发,能够回溯到的最早的位于栈中的节点的 dfn 值(在栈中的元素显然构成一个连通分量);

如果某个点 $u$ 和边 $(u, v)$ 下一个节点 $v$ 满足 $low[v] \ge dfn[u]$,则说明 $u$ 是一个割点;否则 $v$ 一定能通过其它路径和 $u$ 原先所在的连通分量中连通。

从另一个角度而言,如果满足了 $low[v] \ge dfn[u]$,那么把 $u$ 删去后新增的连通块数量就会 +1,这个考虑一下 DFS 的性质就不难证明。所以我们在缩点的过程中统计删去每个点后新增的连通块数量 $cnt[i]$,然后再求一个未删点时总的连通块数量 $sum$,那么最终每个点的答案就是 $sum + cnt[i] - 1$.

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 300050;

vector<int> G[maxn];

namespace CutPoint {
	int dcnt = 0, root = 0,sum=0;
	int dfn[maxn], low[maxn],cnt[maxn];
	bool iscut[maxn];
	vector<int> P;
	void tarjan(int x) {
		dfn[x] = low[x] = ++dcnt;
		int child_count = 0;
		for (unsigned i = 0; i < G[x].size(); i++) {
			int y = G[x][i];
			if (!dfn[y]) {
				tarjan(y);
				low[x] = min(low[x], low[y]);
				if (low[y] >= dfn[x]) {
					child_count++, cnt[x]++;
					if (x != root || child_count > 1)
						iscut[x] = 1;
				}
			} else {
				low[x] = min(low[x], dfn[y]);
			}
		}
	}
	void solve(int n) {
		for (int i = 1; i <= n; i++)
            cnt[i] = 1;
		sum = 0;
		for (int i = 1; i <= n; i++)
			if (!dfn[i])
				sum++, cnt[i] = 0, root = i, tarjan(i);
		for (int i = 1; i <= n; i++)
			printf("%d ",sum + cnt[i] - 1);
	}
}

using CutPoint::solve;
using CutPoint::iscut;
using CutPoint::low;

int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 0, u, v; i < m; i++) {
		scanf("%d%d", &u, &v);
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	solve(n);
	return 0;
}

E. Phone Network

题意

有一个长度为 $n$ 的数列,$a_i$ 的取值可能是 $[1, m]$ 中的整数。对于所有的整数 $i \in [1, m]$,求最短的区间 $[L_i, R_i]$,使得 $[1, i]$ 这 $i$ 个数字在 $[L_i, R_i]$ 中均出现过,输出最短的区间长度。

思路

为了求出满足条件的最短区间,我们首先需要知道对于每个不同的 $i$,满足条件的区间是那些。为此我们需要维护一个量 $R_{i, l}$,表示以 $l$ 为左端点、包含 $[1, i]$ 中所有数的最短区间的右端点。

为了求 $R_{i, l}$,我们首先预处理出每个数 $i$ 在原序列中的所有出现位置 $pos[i]$。但直接暴力计算每个 $R_{i, l}$ 的复杂度是不可接受的,所以我们考虑用数据结构来维护 $R_{i, l}$。

同时我们注意到 $R_{i, l}$ 具有单调性 —— 当 $i, l$ 增大时, $R_{i, l}$ 一定是非严格单调递增的。因为:

  • 如果以 $l$ 为左端点、包含 $[1, i]$ 中所有数的最短区间右端点是 $R_{i, l}$,那么 $R_{i+1, l}$ 要么比 $R_{i, l}$ 更大(说明 $i+1$ 出现在当前区间的更后面),要么和 $R_{i, l}$ 一样长($i+1$ 出现在这个区间当中)。
  • 如果以 $l$ 为左端点时的答案为 $R_{i, l}$,此时以 $l+1$ 为左端点,相当于从区间中删除了 $a_l$,因此区间右端点只可能会右移或不变。

假设我们已经计算了 $R_{i, 1}$ … $R_{i, n}$ 的值,要计算 $R_{i+1, 1} … R_{i+1, n}$。我们已经知道了数 $i+1$ 的在原序列中的全部出现位置,那么对于相邻的两次出现,$p_i, p_j$,对于所有的 $l \in [p_i + 1, p_j]$,就有 $R_{i+1, l} = max(R_{i, l}, p_j)$.

我们可以用线段树在区间 $[1, n]$ 上维护 $R_{i, l}$:

假设线段树中当前维护的是 $R_{i, *}$ 此时要计算 $R_{i+1, *}$, 因为 $R_{i, l}$ 是单调不减的,那么对于区间 $[p_j + 1, p_{j+1}]$,我们在线段树上二分查找到第一个位置 $pos$,使得 $R_{i, pos}$ 到 $R_{i, p_j}$ 的值都大于等于 $p_{j+1}$,然后对 $[p_j + 1, pos]$ 这段 $R$ 值小于 $p{j+1}$ 的区间,使用线段树区间赋值为 $p{j+1}$。

特别地,对于 $i+1$ 的最后一次出现 $p$,如果 $p < n$,那么我们可以将 $[p+1, n]$ 区间赋值为 $+\infty$。对 $i+1$ 的所有的出现位置进行区间赋值操作之后,就得到了 $R_{i+1, *}$。

但是到这里还没有完,最后我们要的答案是 $\min {R_{i+1, l} - l + 1}$,而我们要怎么求这个 min 值呢?

注意到每次赋值的时候,都会将一段区间 $l \in [p_j+1, pos]$ 的 $R_{i+1, l}$ 更新为 $p_{j+1}$,那么这段区间的最优答案其实就是选择从 $pos$ 到 $p_{j+1}$ 这段区间(在右端点相同的情况下,尽可能选择右边的左端点会使得区间长度更小)。因此我们对线段树的每个节点再维护一个答案 $res$,每次赋值时更新当前节点答案为 $res - r + 1$($r$ 为当前节点所代表区间的右端点)即可,最后根节点的 $res$ 值即为答案。

因此我们需要一个支持区间赋值、区间取最小值线段树来完成以上操作。总时间复杂度为 $O(n\log n)$.

代码

#include <bits/stdc++.h>
using namespace std;

const int maxn = 200050;
const int inf = 0x3f3f3f3f;

int a[maxn];
vector<int> p[maxn];

class SegTree {
private:
	int mn[maxn << 2], res[maxn << 2], lp[maxn << 2], rp[maxn << 2], asgn[maxn << 2];
public:
	#define lc (rt << 1)
	#define rc (rt << 1 | 1)
	void pushUp(int rt) {
		mn[rt] = min(mn[lc], mn[rc]);
		res[rt] = min(res[lc], res[rc]);
	}
	void pushDown(int rt) {
		if (!asgn[rt]) 
			return;
		asgn[lc] = asgn[rc] = asgn[rt];
		mn[lc] = mn[rc] = asgn[rt];
		res[lc] = mn[lc] - rp[lc] + 1;
		res[rc] = mn[rc] - rp[rc] + 1;
		asgn[rt] = 0;
	}
	void build(int rt, int l, int r) {
		lp[rt] = l, rp[rt] = r;
		if (l == r) {
			mn[rt] = 0, res[rt] = inf, asgn[rt] = 0;
			return;
		}
		int mid = (l + r) >> 1;
		build(lc, l, mid), build(rc, mid + 1, r);
		pushUp(rt);
	}
	void assign(int rt, int from, int to, int val) {
		if (lp[rt] > to or rp[rt] < from)
			return;
		if (from <= lp[rt] and rp[rt] <= to) {
			mn[rt] = val, asgn[rt] = val;
			res[rt] = val - rp[rt] + 1;
			return;
		}
		pushDown(rt);
		assign(lc, from, to, val), assign(rc, from, to, val);
		pushUp(rt);
	}
	int search(int rt, int from, int to, int val) {
		if (lp[rt] > to or rp[rt] < from)
			return -1;
		if (lp[rt] == rp[rt])
			return mn[rt] <= val ? lp[rt] : -1;
		pushDown(rt);
		int pos = -1;
		if (mn[rc] <= val)
			pos = search(rc, from, to, val);
		if (pos == -1)
			pos = search(lc, from, to, val);
		return pos;
	}
	int query() {
		return res[1];
	}
} T;


template<class T>void read(T &x) {
	T a = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
		f = ch == '-' ? -1 : f, ch = getchar();
	while (ch >= '0' && ch <= '9')
		a = a * 10 + ch - '0', ch = getchar();
	x = a * f;
}

int main() {
	int n, m;
	read(n), read(m);
	for (int i = 1; i <= n; i++)
		read(a[i]), p[a[i]].emplace_back(i);
	for (int i = 1; i <= m; i++)
		sort(p[i].begin(), p[i].end());
		
	T.build(1, 1, n);
	for (int i = 1; i <= m; i++) {
		int j, l;
		for (j = 0, l = 1; j < (int)p[i].size(); j++) {
			int r = p[i][j];
			int pos = T.search(1, l, r, r);
			T.assign(1, l, pos, r);
			l = r + 1;
		}
		if (p[i][p[i].size()-1] != n)
			T.assign(1, l, n, inf);
		printf("%d ", T.query());
	}
	return 0;
}

F. Design Problemset

题意

有 $k$ 种问题,第 $i$ 种问题的个数为 $a_i$ 个。你希望将这些问题分组成若干个问题集合,使得每个问题集满足以下条件:

  • 每个集合的问题总个数在区间 $[L, R]$ 内
  • 每个集合中,第 $i$ 种问题的数量在区间 $[l_i, r_i]$ 内
  • 每个问题只能用一次

求最多能组成多少个不同的问题集。

思路

二分答案+贪心判定。首先能构成的问题集合数量落在区间 $[0, \sum a_i]$ 中,而且满足单调性,即:假如能用这些给定的问题构成 $x+1$ 集合,那么也一定能用它们构成 $x$ 个集合(扔掉一个集合不要就行了)。

所以我们二分这个区间,每次取中值 $mid$,判断是否能够构成 $mid$ 个集合。判断的时候,我们可以首先贪心地将每组问题的 $l_i$ 平摊到每个集合中,如果 $l_i \times mid < a_i$,显然无法构成 $mid$ 个集合;如果 $\sum l_i > R$,同样也无法构成;如果 $L \le \sum l_i \le R$ 则可以构成。

接下来考虑 $\sum l_i < L$ 的情况,此时我们还能再为每个集合分配剩下还有配额可用的问题:枚举问题种类 $i$,第 $i$ 类问题中可以分配的数量还剩下 $r = \min \{(r_i - l_i) \times mid, a_i - l_i \times mid \}$。

然后我们将这 $r$ 个问题每次一个放到 $mid$ 个集合中,把这 $r$ 个问题放完后,有 $mid - (r \mod mid)$ 集合增加了 $\lfloor \frac{r}{mid} \rfloor$ 个问题,有 $r \mod mid$ 个集合增加了 $\lfloor \frac{r}{mid} \rfloor + 1$ 个问题,使用类似加法进位的思路模拟即可。

最后这道题会爆 long long,需要用 unsigned long long 或者 __int128 才能通过。

二分答案的时间复杂度为 $O(\log \sum a_i)$,每次判断的时间复杂度为 $O(k)$,总时间复杂度为 $O(k \log \sum a_i)$.

代码

#include <bits/stdc++.h>
using namespace std;
using ll = unsigned long long;
const int maxn = 1000050;
int k;
ll L, R, l[maxn], r[maxn], a[maxn];

bool check(ll ans) {
	ll tot = 0;
	for (int i = 0; i < k; i++) {
		if (a[i] < ans * l[i])
			return false;
		tot += l[i];
	}
	if (tot > R)
		return false;
	else if (tot < L) {
		ll rem = L - tot, xx = 0, cur = 0;
		for (int i = 0; i < k; i++) {
			ll lm = min((r[i] - l[i]) * ans, a[i] - l[i] * ans);
			xx += lm;
			cur += xx / ans;
			xx %= ans;
			if (cur >= rem)
				return true;
		}
		return false;
	}
	else
		return true;
}
int main() {
	ll lim = 0, sum = 0;
	cin >> k >> L >> R;
	for (int i = 0; i < k; i++)
		cin >> a[i], sum += a[i];
	lim = sum;
	for (int i = 0; i < k; i++)
		cin >> l[i], cin >> r[i];
	ll x = 0, y = lim, ans = 0;
	while (x + 1 < y) 
	{
		ll mid = (x + y) >> 1;
		if (check(mid)) {
			x = mid;
		} else
			y = mid;
	}
	if (check(y))
		cout << y << endl;
	else
		cout << x << endl;
	return 0;
}

G. Tree Projection

题意

给定两个 $1, …, n$ 的排列 $A$ 和 $B$,构造一棵树,使得这棵树以 $A_1$ 为根时的拓扑序为 $A$,以 $B_1$ 为根时 DFS 序为 $B$.

思路

这样的树一定存在。记录排列 $B$ 中的每个数 $B_i$ 在 $A$ 排列中的位置,记为 $t_{B_i}$,初始时令根节点 $rt$ 为 $B_1$。

枚举 $B_2, …, B_n$ 中的每个点,然后将它与 $rt$ 连边。如果 $t_{B_i} < t_{rt}$,就换根令 $B_i$ 作为新根。这样构造出来的树就一定满足条件。

证明:

考虑排列 $A$,对于每一条边 $(rt, B_i)$,可以知道 $rt$ 和 $B_i$ 谁在排列 $A$ 中更靠前,谁就离 $A_1$ 更近。也就是说,在 $A$ 中靠前的点是靠后的点的父亲。
考虑排列 $B$,当 $B_1$ 为根时,对于 $i \ge 2, B_i$ 要么是叶子节点,要么 $B_{i+1}…B_n$ 都可以是 $B_{i}$ 的子孙。也就是说每个点的子树都在排列 $B$ 的某个子区间中。

代码

#include <bits/stdc++.h> 
using namespace std;

const int maxn = 200050;

int a[maxn], b[maxn], topo[maxn];
vector<pair<int, int>> e;

template<class T>void read(T &x) {
	T a = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
		f = ch == '-' ? -1 : f, ch = getchar();
	while (ch >= '0' && ch <= '9')
		a = a * 10 + ch - '0', ch = getchar();
	x = a * f;
}

int main() {
	int n;
	read(n);
	for (int i = 0; i < n; i++)
		read(a[i]), topo[a[i]] = i + 1;
	for (int i = 0; i < n; i++)
		read(b[i]);
	int rt = b[0];
	for (int i = 1; i < n; i++) {
		e.emplace_back(make_pair(rt, b[i]));
		if (topo[rt] > topo[b[i]])
			rt = b[i];
	}
	printf("YES\n");
	for (unsigned i = 0; i < e.size(); i++)
		printf("%d %d\n", e[i].first, e[i].second);
	return 0;
}

H. Grouping

题意

将 $2n$ 个数分成 $n$ 组,定义一种分组方案的权值为:所有组的方差之和。求分组方案的权值期望,答案对 998244353 取模。

思路

不会(理直气壮)。

I. Walking Machine

题意

给一个 $n \times m$ 的迷宫,每个格子只能向 W, A, S, D 中一个确定的方向移动,问从多少个格子出发最终会走出边界。

思路

签到难度的 DFS,打好访问标记即可。

代码

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int x=0,f=1; char c=getchar();
	while (c<'0' ||c>'9') {if (c=='-') f=-1; c=getchar();}
	while (c>='0' && c<='9') x=x*10+c-'0',c=getchar();
	return x*f;
}
int n, m, ans, cnt;
int flag[1010][1010];
char str[1010][1010];

bool dfs(int x, int y) {
	if (x <= n && x > 0 && y <= m && y > 0) {
		if (flag[x][y]) {
			if (flag[x][y] == 2) {
				ans += cnt;
				return 1;
			}
			return 0;
		}
		flag[x][y] = 1, cnt++;
	}
	else {
		ans += cnt;
		return 1;
	}
	bool xx;
	if (str[x][y] == 'S') xx = dfs(x+1,y);
	if (str[x][y] == 'W') xx = dfs(x-1,y);
	if (str[x][y] == 'A') xx = dfs(x,y-1);
	if (str[x][y] == 'D') xx = dfs(x,y+1);
	if (xx)
		flag[x][y] = 2;
	return xx;
}
int main() {
	n = read(); m = read();
	for (int i = 1; i <= n; i++)
		scanf("%s",str[i] + 1);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			if (!flag[i][j])
				cnt = 0, dfs(i, j);		
	cout << ans;
	return 0;
}

J. Matrix Subtraction

题意

给定一个 $n \times m$ 的矩阵 $M$,矩阵中的每个位置都有一个权值 $c_{ij}$。每次你可以选择一个 $a \times b$ 大小的子矩阵,将这个子矩阵中所有的点权值 -1. 问经过若干次操作后,能否将矩阵 $M$ 变为全 0 矩阵。

思路

用二维树状数组维护矩阵,初始时矩阵全 0,从左上角按行序枚举每个点 $(i, j)$,对大小为 $a \times b$ 的子矩阵区间加上 $c_{ij} - query(i, j)$,最后判断树状数组中的值与 $M$ 是否相同即可。如果更新的时候遇到了负数则可以直接退出回答不能。

代码

#include<bits/stdc++.h>
#define N 1010
using namespace std;
inline int read()
{
	int x=0,f=1; char c=getchar();
	while (c<'0' ||c>'9') {if (c=='-') f=-1; c=getchar();}
	while (c>='0' && c<='9') x=x*10+c-'0',c=getchar();
	return x*f;
}
class T
{
	public:
		int c[N][N];
		T(){memset(c,0,sizeof(c));}
		void init(){memset(c,0,sizeof(c));} 
		int lowbit(int x){return x&(-x);}
		void add(int x,int y,int num,int n,int m)
		{
			for (int i=x;i<=n;i+=lowbit(i))
				for (int j=y;j<=m;j+=lowbit(j))
					c[i][j]+=num;
		}
		int GetSum(int x,int y)
		{
			int total=0;
			for (int i=x;i>0;i-=lowbit(i))
				for (int j=y;j>0;j-=lowbit(j))
					total+=c[i][j];
			return total;
		}
		int query(int x1,int y1,int x2,int y2){
			return GetSum(x2,y2)-GetSum(x1-1,y2)-GetSum(x2,y1-1)+GetSum(x1-1,y1-1);
		}
}T;
int n,m,a,b;
int s[N][N];
int Test;
int main()
{
	Test=read();
	while (Test--)
	{ 
		n=read(); m=read(); a=read(); b=read();
		T.init();
		for (int i=1;i<=n;i++)
			for (int j=1;j<=m;j++)
				s[i][j]=read();
		bool flag=1;
		for (int i=1;i+a-1<=n;i++)
			for (int j=1;j+b-1<=m;j++)
			{
				int x=T.query(1,1,i,j);
				if (s[i][j]<x)
				{
					flag=0;
					break;
				}
				T.add(i,j,s[i][j]-x,n,m);
				T.add(i,j+b,-(s[i][j]-x),n,m);
				T.add(i+a,j,-(s[i][j]-x),n,m);
				T.add(i+a,j+b,s[i][j]-x,n,m);
			}
		for (int i=1;i<=n;i++)
			for (int j=1;j<=m;j++)
			{
				int x=T.query(1,1,i,j);
				if (x!=s[i][j])
				{
					flag=0;
					break;
				}
			}
		if (flag) printf("^_^\n"); else printf("QAQ\n");
	} 
}

K. Sqrt Approaching

题意

给定三个巨大的整数 $A, B, n$,找到两个整数 $C, D$ 满足 $(BC-AD)(C-D\sqrt{n}) < 0$.

思路

假设 $\frac{A}{B} < \sqrt{n}$, 将式子变形成 $\frac{A}{B} < \frac{C}{D} < \sqrt{n}$ 的形式,然后……总之经过各种放缩之后可以得到 $C = n \times (A+B)$,$D = n \times B + A$,然后用 Python 或者 Java 的大整数整整就过了……了……

代码

a = int(input())
b = int(input())
n = int(input())
print(n * (a + b))
print(n * b + a)