2021CCPC江西省赛题解ABGHIJKL

2021CCPC江西省赛题解ABGHIJKL

K. Many Littles Make a Mickle

题意

t    ( 1 ≤ t ≤ 100 ) t\ \ (1\leq t\leq 100) t  (1t100)组测试数据.每组测试数据输入两个整数 n , m    ( 1 ≤ n , m ≤ 100 ) n,m\ \ (1\leq n,m\leq 100) n,m  (1n,m100).

对每组测试数据,输出 m ∑ i = 1 n i 2 \displaystyle m\sum_{i=1}^n i^2 mi=1ni2.

思路

∑ i = 1 n i 2 = n ( n + 1 ) ( 2 n − 1 ) 6 \displaystyle \sum_{i=1}^n i^2=\dfrac{n(n+1)(2n-1)}{6} i=1ni2=6n(n+1)(2n1).

最终答案是 O ( n 3 m ) O(n^3m) O(n3m)级别的,不会爆int.

代码 -> 2021CCPC江西省赛题解-K(推公式)

void solve() {
	int n, m; cin >> n >> m;
	cout << n * (n + 1) * (2 * n + 1) / 6 * m << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


B. Continued Fraction

题意

给定一个既约分数 x y \dfrac{x}{y} yx,将其化为连分数的形式 a 0 + 1 a 1 + 1 a 2 + 1 ⋱ + 1 a n a_0+\dfrac{1}{a_1+\dfrac{1}{a_2+\dfrac{1}{\ddots+\dfrac{1}{a_n}}}} a0+a1+a2++an1111.

t    ( 1 ≤ t ≤ 1000 ) t\ \ (1\leq t\leq 1000) t  (1t1000)组测试数据.每组测试数据输入两个整数 x , y    ( 1 ≤ x , y ≤ 1 e 9 , gcd ⁡ ( x , y ) = 1 ) x,y\ \ (1\leq x,y\leq 1\mathrm{e}9,\gcd(x,y)=1) x,y  (1x,y1e9,gcd(x,y)=1).

对每组测试数据,先输出 n    ( 0 ≤ n ≤ 100 ) n\ \ (0\leq n\leq 100) n  (0n100),再输出 a 0 , ⋯   , a n    ( 0 ≤ a i ≤ 1 e 9 ) a_0,\cdots,a_n\ \ (0\leq a_i\leq 1\mathrm{e}9) a0,,an  (0ai1e9).若有多组解,输出任一组.

思路

①若 y ∣ x y\mid x yx,即 x y \dfrac{x}{y} yx是整数时,它已是连分数.

②若 x y \dfrac{x}{y} yx是假分数,提出 a 0 = ⌊ x y ⌋ a_0=\left\lfloor\dfrac{x}{y}\right\rfloor a0=yx即可变为真分数,故只需考虑真分数的情况.

③若 x y \dfrac{x}{y} yx是真分数,注意到 x y = 1 y x \dfrac{x}{y}=\dfrac{1}{\dfrac{y}{x}} yx=xy1,其中 y x \dfrac{y}{x} xy是假分数,递归处理即可.递归终止条件: x y \dfrac{x}{y} yx是整数.

事实上,②和③可统一为 x y = ⌊ x y ⌋ + x   m o d   y y = ⌊ x y ⌋ + 1 y x   m o d   y \dfrac{x}{y}=\left\lfloor\dfrac{x}{y}\right\rfloor+\dfrac{x\ \mathrm{mod}\ y}{y}=\left\lfloor\dfrac{x}{y}\right\rfloor+\dfrac{1}{\dfrac{y}{x\ \mathrm{mod}\ y}} yx=yx+yx mod y=yx+x mod yy1.

该过程类似于辗转相除法,时间复杂度 O ( log ⁡ max ⁡ { x , y } ) O(\log\max\{x,y\}) O(logmax{x,y}).

代码 -> 2021CCPC江西省赛题解-B(连分数+递归)

void cal(int x, int y, vi& res) {
	res.push_back(x / y);
	if (x % y == 0) return;  // 递归终止条件

	cal(y, x % y, res);
}

void solve() {
	int x, y; cin >> x >> y;

	vi ans;
	cal(x, y, ans);

	cout << ans.size() - 1 << ' ';  // 注意-1
	for (auto i : ans) cout << i << ' ';
	cout << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


L. It Rains Again

题意 ( 2   s 2\ \mathrm{s} 2 s)

在平面直角坐标系 x O y xOy xOy中, x x x轴代表地面.平面上有 n n n个不考虑厚度的挡雨板,每个挡雨板可视为一条线段.现从无限高的地方开始下雨,雨只能竖直下落,问 x x x轴上有多少地方不会被雨淋到.

第第一行输入一个整数 n    ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n  (1n1e5).接下来 n n n行每行输入四个整数 x 1 , y 1 , x 2 , y 2    ( 1 ≤ x 1 < x 2 ≤ 1 e 5 , 1 ≤ y 1 , y 2 ≤ 1 e 5 ) x_1,y_1,x_2,y_2\ \ (1\leq x_1<x_2\leq 1\mathrm{e}5,1\leq y_1,y_2\leq 1\mathrm{e}5) x1,y1,x2,y2  (1x1<x21e5,1y1,y21e5),表示有一个可视为从连接点 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)与点 ( x 2 , y 2 ) (x_2,y_2) (x2,y2)的线段的挡雨板.注意两挡雨板可能重叠、相交.数据保证没有挡雨板竖直放置.

思路I

显然只需考虑每个挡雨板在 x x x轴上的投影,转化为求 n n n个区间 [ x 1 , x 2 ] [x_1,x_2] [x1,x2]的并的长度之和.

代码I -> 2021CCPC江西省赛题解-L(区间并)

void solve() {
	int n; cin >> n;

	vii segs(n);
	for (int i = 0; i < n; i++) {
		int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
		segs[i] = { min(x1,x2),max(x1,x2) };
	}

	sort(all(segs));

	int ans = 0;
	int L = -INF, R = -INF;  // 当前区间的左右端点
	int i = 0;
	while (i < n) {
		auto [l, r] = segs[i];
		if (l > R) L = l, R = r;  // 当前的区间与上一区间无交集

		while (i < n && segs[i].first <= R) R = max(R, segs[i++].second);
		ans += R - L;
	}
	cout << ans;
}

int main() {
	solve();
}

思路II

x , y x,y x,y范围小,可用差分+前缀和求区间并.

代码II -> 2021CCPC江西省赛题解-L(差分+前缀和)

const int MAXN = 1e5 + 5;
int diff[MAXN];

void solve() {
	int n; cin >> n;
	for (int i = 0; i < n; i++) {
		int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
		diff[x1]++, diff[x2]--;
	}

	int ans = 0;
	for (int i = 1; i <= 1e5; i++) {
		diff[i] += diff[i - 1];
		if (diff[i]) ans++;
	}
	cout << ans;
}

int main() {
	solve();
}

上为差分+前缀和,下为区间并.反直觉的事情是区间并的做法所需的空间更多.

在这里插入图片描述



H. Hearthstone So Easy

题意 ( 2   s 2\ \mathrm{s} 2 s)

两玩家玩卡牌游戏,每轮的情况为:玩家 1 1 1抽卡,玩家 1 1 1出牌,玩家 2 2 2抽卡,玩家 2 2 2出牌.对玩家出牌的阶段,可选择令自己的血量 + = k +=k +=k或让对方的血量 − = k -=k =k,注意两玩家的血量无上限,初始时两玩家的血量都为 n n n,玩家血量 ≤ 0 \leq 0 0时立即失败.若玩家手中无卡会进入疲劳状态,每抽一张卡需增加 1 1 1点自己的疲劳值,且会从生命中扣除等量的疲劳值,初始时两玩家的疲劳值都为 0 0 0.初始时两玩家手中都无卡.现pllj与freesin玩该游戏,pllj先手,两人都采取最优策略,问最后谁获胜.

t    ( 1 ≤ t ≤ 1 e 5 ) t \ \ (1\leq t\leq 1\mathrm{e}5) t  (1t1e5)组测试数据.每组测试数据输入两个整数 n , k    ( 1 ≤ n , k ≤ 1 e 9 ) n,k\ \ (1\leq n,k\leq 1\mathrm{e}9) n,k  (1n,k1e9).

思路

显然 n = 1 n=1 n=1时pllj抽一张卡后即失败.

n ≥ 2 n\geq 2 n2的情况,显然两玩家都是抽一张卡打一张卡,则pllj若在第一轮无法打败freesin,则后续的轮中freesin每次都回血,因pllj先扣疲劳值,故最后freesin胜.故先手必胜的充要条件是: n ≤ k + 1 n\leq k+1 nk+1,其中 k + 1 k+1 k+1是因为两人血量都为 ( k + 1 ) (k+1) (k+1)时,pllj第一轮先攻击,第二轮freesin抽卡后失败.

代码 -> 2021CCPC江西省赛题解-H(思维)

void solve() {
	int n, k; cin >> n >> k;
	
	if (n > 1 && n <= k + 1) cout << "pllj" << endl;
	else cout << "freesin" << endl;
}

int main() {
	CaseT  // 单测时注释掉该行
	solve();
}


A. Mio visits ACGN Exhibition

题意 ( 2   s , 256   M B 2\ \mathrm{s},256\ \mathrm{MB} 2 s,256 MB)

给定一个 n × m n\times m n×m 0 − 1 0-1 01矩阵,从左上角 ( 1 , 1 ) (1,1) (1,1)出发,到达右下角 ( n , m ) (n,m) (n,m),每次只能向右或向下移动到相邻的各自.问有几条不同的路径使得路径至少经过 p p p 0 0 0 q q q 1 1 1,答案对 998244353 998244353 998244353取模.

第一行输入四个整数 n , m , p , q    ( 1 ≤ n , m ≤ 500 , 0 ≤ p , q ≤ 1 e 4 ) n,m,p,q\ \ (1\leq n,m\leq 500,0\leq p,q\leq 1\mathrm{e}4) n,m,p,q  (1n,m500,0p,q1e4).接下来输入一个 n × m n\times m n×m 0 − 1 0-1 01矩阵.

思路

d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示从 ( 1 , 1 ) (1,1) (1,1) ( i , j ) (i,j) (i,j)恰经过 k k k 0 0 0的路径数,最终答案为 ∑ k = p n + m − 1 − q d p [ n ] [ m ] [ k ] \displaystyle\sum_{k=p}^{n+m-1-q}dp[n][m][k] k=pn+m1qdp[n][m][k].初始条件 d p [ 1 ] [ 1 ] [ ! a [ 1 ] [ 1 ] ] = 1 dp[1][1][!a[1][1]]=1 dp[1][1][!a[1][1]]=1,注意不是 d p [ 1 ] [ 1 ] [ a [ 1 ] [ 1 ] ] = 1 dp[1][1][a[1][1]]=1 dp[1][1][a[1][1]]=1.

一个int类型的该数组所需空间 4 × 500 × 500 × 1 e 4 1024 × 1024 ≈ 1 e 4   M B > 256   M B \dfrac{4\times 500\times 500\times 1\mathrm{e}4}{1024\times 1024}\approx 1\mathrm{e}4\ \mathrm{MB}>256\ \mathrm{MB} 1024×10244×500×500×1e41e4 MB>256 MB, d p dp dp数组的第一维滚动即可.

代码 -> 2021CCPC江西省赛题解-A(线性DP+滚动数组优化)

const int MAXN = 505, MAXM = 1e4 + 5;
const int MOD = 998244353;
int n, m, p, q;
int a[MAXN][MAXN];
int dp[2][MAXN][MAXM];  // dp[i][j][k]表示从(1,1)到(i,j)恰经过k个0的路径数

void solve() {
	cin >> n >> m >> p >> q;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++) cin >> a[i][j];

	dp[1][1][!a[1][1]] = 1;  // 初始条件
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			if (i == 1 && j == 1) continue;

			for (int k = 0; k <= i + j - 1; k++) {  // 枚举0的个数
				if (a[i][j]) dp[i & 1][j][k] = ((ll)dp[i & 1][j - 1][k] + dp[i - 1 & 1][j][k]) % MOD;
				else {
					dp[i & 1][j][k + 1] = ((ll)dp[i & 1][j - 1][k] + dp[i - 1 & 1][j][k]) % MOD;
					dp[i & 1][j][0] = 0;  // 清空非法状态
				}
			}
		}
	}

	int ans = 0;
	for (int k = p; k <= n + m - 1 - q; k++) ans = ((ll)ans + dp[n & 1][m][k]) % MOD;
	cout << ans;
}

int main() {
	solve();
}


J. LRU

题意 ( 3   s 3\ \mathrm{s} 3 s)

称一个cache的容量为 k k k,如果它至多能同时存放 k k k个单位的数据.现有规则:①若要访问的数据在cache中,则发生cache hit,否则发生cache miss;②若要访问的数据不在cache中,且cache未满,将其加入cache中;③若要访问的数据不在cache中,且cache已满,则将cache中距离访问时间最久的数据替换为当前的要访问的数据.

给定 n n n个要访问的数据的编号 a 1 , ⋯   , a n    ( 1 ≤ a i ≤ 1 e 9 ) a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}9) a1,,an  (1ai1e9),问cache的容量至少为多少时能保证至少发生 k    ( 1 ≤ k ≤ n ≤ 1 e 5 ) k\ \ (1\leq k\leq n\leq 1\mathrm{e}5) k  (1kn1e5)次cache hit,若无法发发生,输出"cbddl".

思路

显然二分,考虑如何check.用一个set<pair<int,int>>维护每个数据的访问时间,其中first为数据的访问时间,second为数据编号,这样set内部默认按访问时间升序排列.用一个map<int,int>记录每个数据的访问时间,方便从set中删除数据,其中second为 0 0 0表示该数据不在cache中.

注意二分右边界取 n + 1 n+1 n+1,防止结果为 n n n时无法判断是否有解.

代码 -> 2021CCPC江西省赛题解-J(二分+STL)

const int MAXN = 1e5 + 5;
int n, k;
int a[MAXN];

bool check(int capa) {
	map<int, int> mp;  // mp[i]=j表示数据i的访问时间为j
	set<pii> s;  // first表示数据的访问时间,second表示数据的编号
	int cnt = 0;  // cache hit数
	for (int i = 1; i <= n; i++) {
		if (mp[a[i]]) {  // cache hit
			cnt++;

			// 更新该数据的访问时间
			s.erase({ mp[a[i]],a[i] });
			mp[a[i]] = i;
			s.insert({ mp[a[i]],a[i] });
		}
		else {  // cache miss
			if (s.size() < capa) {  // cache未满
				mp[a[i]] = i;
				s.insert({ mp[a[i]],a[i] });
			}
			else {  // cache已满
				auto tmp = *s.begin();  // 距离访问时间最久的数据
				s.erase(s.begin());
				mp[tmp.second] = 0;  // 将该数据移出cache
				
				mp[a[i]] = i;
				s.insert({ mp[a[i]],a[i] });
			}
		}
	}
	return cnt >= k;
}

void solve() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) cin >> a[i];

	int l = 1, r = n + 1;
	while (l < r) {
		int mid = l + r >> 1;
		if (check(mid)) r = mid;
		else l = mid + 1;
	}
	cout << (l == n + 1 ? "cbddl" : to_string(l));
}

int main() {
	solve();
}


G. Magic Number Group

题意

给定一个长度为 n n n的正整数序列 a 1 , ⋯   , a n a_1,\cdots,a_n a1,,an.现有询问:指定一个区间 [ l , r ] [l,r] [l,r],选定一个正整数 p ≥ 2 p\geq 2 p2,求 a [ l ⋯ r ] a[l\cdots r] a[lr]中能被 p p p整除的数的个数的最大值.

t    ( 1 ≤ t ≤ 5 e 4 ) t\ \ (1\leq t\leq 5\mathrm{e}4) t  (1t5e4)组测试数据.每组测试数据第一行输入两个整数 n , q    ( 1 ≤ n , q ≤ 5 e 4 ) n,q\ \ (1\leq n,q\leq 5\mathrm{e}4) n,q  (1n,q5e4),分别表示序列长度和询问书.第二行输入 n n n个整数 a 1 , ⋯   , a n    ( 1 ≤ a i ≤ 1 e 6 ) a_1,\cdots,a_n\ \ (1\leq a_i\leq 1\mathrm{e}6) a1,,an  (1ai1e6).接下来 q q q行每行输入两个整数 l , r    ( 1 ≤ l , r ≤ n ) l,r \ \ (1\leq l,r\leq n) l,r  (1l,rn),表示一个询问.数据保证所有测试数据的 n n n之和、 q q q之和都不超过 5 e 4 5\mathrm{e}4 5e4.

思路

显然 p p p取素数最优.注意到 2 × 3 × 5 × 7 × 11 × 13 < 1 e 6 , 2 × 3 × 5 × 7 × 11 × 13 × 17 > 1 e 6 2\times 3\times 5\times 7\times 11\times 13<1\mathrm{e}6,2\times 3\times 5\times 7\times 11\times 13\times 17>1\mathrm{e}6 2×3×5×7×11×13<1e6,2×3×5×7×11×13×17>1e6,故 1 e 6 1\mathrm{e}6 1e6内的数至多有 7 7 7个不同的素因子,可用朴素的筛法预处理出,注意不能用线性筛,因为线性筛中每个数只会被其最小素因子筛掉.

类似于莫队维护区间众数, c n t [ p ] cnt[p] cnt[p]表示素因子 p p p出现的次数, s u m [ i ] sum[i] sum[i]表示出现次数为 i i i的素因子数,用莫队维护区间素因子出现次数最大值即可.设 1 e 6 1\mathrm{e}6 1e6内的数的素因子个数最大值为 k k k,因 n n n q q q同数量级,时间复杂度 O ( k n n ) O(kn\sqrt{n}) O(knn ).

代码 -> 2021CCPC江西省赛题解-G(简单数论+莫队)

const int MAXN = 1e6 + 5;
int n, q;
int len;  // 分块长度
int a[MAXN];
int block[MAXN];  // block[i]表示下标i所在分块的编号
struct Query {
	int l, r, id;  // 询问区间、编号

	bool operator<(const Query& B)const {
		return block[l] ^ block[B.l] ? l < B.l : block[l] & 1 ? r < B.r : r > B.r;
	}
}ques[MAXN];
int cnt[MAXN];  // cnt[p]表示素因子p出现的次数
int sum[MAXN];  // sum[i]表示出现次数为i的素因子数
int res;  // 当前答案
int ans[MAXN];

bool vis[MAXN];
vi fac[MAXN];  // 存每个数的素因子

void init() {
	for (int i = 2; i < MAXN; i++) {
		if (!vis[i]) {
			for (int j = i; j < MAXN; j += i) {
				vis[j] = true;
				fac[j].push_back(i);
			}
		}
	}
}

void insert(int x) {
	for (auto p : fac[a[x]]) {
		sum[cnt[p]++]--;  // 素因子p旧的出现次数--
		sum[cnt[p]]++;  // 素因子p新的出现次数++
		res = max(res, cnt[p]);  // 更新区间素因子出现次数最大值
	}
}

void remove(int x) {
	for (auto p : fac[a[x]]) {
		if (--sum[cnt[p]] == 0 && res == cnt[p]) res--;  // 更新区间素因子出现次数最大值
		sum[--cnt[p]]++;
	}
}

void solve() {
	init();

	CaseT{
		cin >> n >> q;

		len = sqrt(n);
		for (int i = 1; i <= n; i++) cin >> a[i];

		for (int i = 1; i <= q; i++) {
			cin >> ques[i].l >> ques[i].r;
			ques[i].id = i;
			block[i] = (i - 1) / len + 1;
		}
		sort(ques + 1, ques + q + 1);

		int l = 1, r = 0;
		for (int i = 1; i <= q; i++) {
			while (l > ques[i].l) insert(--l);
			while (r < ques[i].r) insert(++r);
			while (l < ques[i].l) remove(l++);
			while (r > ques[i].r) remove(r--);
			ans[ques[i].id] = res;
		}

		for (int i = 1; i <= q; i++) cout << ans[i] << endl;

		while (l <= r) remove(l++);  // 清空
	}
}

int main() {
	solve();
}


I. Homework

题意 ( 4 s 4 \mathrm{s} 4s)

有编号 1 ∼ n 1\sim n 1n n n n个同学在各自家中, ( n − 1 ) (n-1) (n1)条双向道路连接 n n n个同学的家.每个同学可选择自己独立完成作业,也可选择抄其他同学的作业.若同学 i i i想抄同学 j j j的作业,则他要先等同学 j j j独立完整作业,然后前往他家抄作业,最后回到自己家,此时视为作业完成.同学 i i i到同学 j j j家抄作业再回到自己家所需时间等于两同学家间的距离(注意不是两倍).

第一行输入两个整数 n , q    ( 1 ≤ n , q ≤ 1 e 5 ) n,q\ \ (1\leq n,q\leq 1\mathrm{e}5) n,q  (1n,q1e5),分别表示学生数和操作数.第二行输入 n n n个整数 a 1 , ⋯   , a n    ( 0 ≤ a i ≤ 1 e 9 ) a_1,\cdots,a_n\ \ (0\leq a_i\leq 1\mathrm{e}9) a1,,an  (0ai1e9),表示每个学生独立完成作业的时间.接下来 ( n − 1 ) (n-1) (n1)行每行输入三个整数 u , v , w     ( 1 ≤ u , v ≤ n , 0 ≤ w ≤ 1 e 9 ) u,v,w\ \ \ (1\leq u,v\leq n,0\leq w\leq 1\mathrm{e}9) u,v,w   (1u,vn,0w1e9),表示有连接同学 u u u家与同学 v v v家的长度为 w w w的双向道路.接下来 q q q行每行输入一个操作,格式如下:① 1   i   x    ( 1 ≤ i ≤ n , 0 ≤ x ≤ 1 e 9 ) 1\ i\ x\ \ (1\leq i\leq n,0\leq x\leq 1\mathrm{e}9) 1 i x  (1in,0x1e9),表示学生 i i i独立完成作业的时间变为 x x x;② 2   i   w 2\ i\ w 2 i w,表示第 i i i条道路的长度变为 w w w;③ 3 3 3,表示询问所有学生完成作业的最早时间,设第 i    ( 1 ≤ i ≤ n ) i\ \ (1\leq i\leq n) i  (1in)个学生完成作业的最早时间为 t i t_i ti,则输出 t 1   x o r   t 2   x o r   ⋯   x o r   t n t_1\ \mathrm{xor}\ t_2\ \mathrm{xor}\ \cdots\ \mathrm{xor}\ t_n t1 xor t2 xor  xor tn.数据保证所有同学能到达其他同学家里,且询问数不超过 200 200 200.

思路

显然修改操作可 O ( 1 ) O(1) O(1)完成,下面讨论查询操作.

同学 u u u独立完成作业的时间为 a u a_u au,他去抄在 t v t_v tv时刻已完成作业的同学 v v v的作业所需时间 d i s ( u , v ) + t v dis(u,v)+t_v dis(u,v)+tv,则 u u u完成作业的最早时间为 min ⁡ { a u , d i s ( u , v ) + t v } \min\{a_u,dis(u,v)+t_v\} min{au,dis(u,v)+tv},其中 d i s ( u , v ) dis(u,v) dis(u,v)表示节点 u u u与节点 v v v的最短距离.朴素做法:对每个节点,枚举已完成作业的节点,检查是否能让自己的时间变短;或对每个已完成作业的节点,检查是否能让其他节点的时间变短.直接求两点间的距离时间复杂度 O ( n 3 ) O(n^3) O(n3),用倍增或树剖求两点间的距离时间复杂度 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn),都会TLE.

注意到上述朴素做法的过程与Dijkstra算法的过程类似.因树上两节点间的简单路径唯一,设从节点 u u u经过节点 p p p到达节点 v v v,若 u u u要松弛 v v v,则 u u u会先松弛 p p p,此时 u u u松弛 v v v等价于 u u u先松弛 p p p, p p p再松弛 v v v,故只需考虑树边,用堆优化的Dijkstra算法时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),也会TLE.

考虑优化,注意到上述过程中每个节点只会被其父亲节点或儿子节点更新,考虑换根DP. d p [ u ] dp[u] dp[u]表示只考虑节点 u u u的子树的前提下同学 u u u完成作业的最早时间,状态转移方程 d p [ u ] = min ⁡ v ∈ s o n u { d p [ v ] + w ( u , v ) } \displaystyle dp[u]=\min_{v\in son_u}\{dp[v]+w(u,v)\} dp[u]=vsonumin{dp[v]+w(u,v)},其中 w ( u , v ) w(u,v) w(u,v)表示节点 u u u与节点 v v v间的边权,初始条件 d p [ u ] = a [ u ] dp[u]=a[u] dp[u]=a[u].第一次DFS以节点 1 1 1为根节点,求出所有 d p [ u ] dp[u] dp[u]后,第二次DFS换根.设节点 v v v是节点 u u u的一个儿子节点,若能用 d p [ v ] dp[v] dp[v]更新 d p [ u ] dp[u] dp[u],则 d p [ v ] ≥ d p [ u ] + w ( u , v ) dp[v]\geq dp[u]+w(u,v) dp[v]dp[u]+w(u,v),即不会出现重复走一条边的情况,故由以节点 u u u为根换为以节点 v v v为根,状态转移方程 d p [ v ] = min ⁡ v ∈ s o n u { d p [ u ] + w ( u , v ) } \displaystyle dp[v]=\min_{v\in son_u}\{dp[u]+w(u,v)\} dp[v]=vsonumin{dp[u]+w(u,v)}.

为方便修改边权,可用链式前向星存图,记下每条道路对应的双向边的编号.但事实上同时加正向边和反向边时第 i i i条道路对应的双向边的编号即 2 ( i − 1 ) 2(i-1) 2(i1) 2 ( i − 1 ) + 1 2(i-1)+1 2(i1)+1.

代码I -> 2021CCPC江西省赛题解-I(链式前向星+换根DP)

const int MAXN = 1e5 + 5, MAXM = MAXN << 1;
int n, q;
int a[MAXN];  // 每个同学独立完成作业的时间
int head[MAXN], edge[MAXM], w[MAXM], nxt[MAXM], idx;
umap<int, pii> mp;  // 每条道路对应的双向边的编号
int dp[MAXN];  // dp[u]表示只考虑节点u的子树的前提下同学u完成作业的最早时间

void add(int a, int b, int c) {
	edge[idx] = b, w[idx] = c, nxt[idx] = head[a], head[a] = idx++;
}

void dfs1(int u, int fa) {  // 当前节点、前驱节点
	dp[u] = a[u];  // 初始化为独立完成作业的时间
	for (int i = head[u]; ~i; i = nxt[i]) {
		int v = edge[i];
		if (v == fa) continue;

		dfs1(v, u);  // 递归求子树的信息
		dp[u] = min(dp[u], dp[v] + w[i]);
	}
}

void dfs2(int u, int fa) {  // 当前节点、前驱节点
	for (int i = head[u]; ~i; i = nxt[i]) {
		int v = edge[i];
		if (v == fa) continue;

		dp[v] = min(dp[v], dp[u] + w[i]);  // 换根
		dfs2(v, u);  // 递归求子树的信息
	}
}

void solve() {
	memset(head, -1, so(head));

	cin >> n >> q;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = 1; i < n; i++) {
		int u, v, w; cin >> u >> v >> w;
		mp[i] = { idx,idx + 1 };
		add(u, v, w), add(v, u, w);
	}

	while (q--) {
		int op; cin >> op;
		if (op == 1) {
			int i, x; cin >> i >> x;
			a[i] = x;
		}
		else if (op == 2) {
			int i, x; cin >> i >> x;
			w[mp[i].first] = w[mp[i].second] = x;
		}
		else {
			dfs1(1, -1);  // 从1号节点开始搜,无前驱节点
			dfs2(1, -1);  // 从1号节点开始搜,无前驱节点

			int ans = 0;
			for (int u = 1; u <= n; u++) ans ^= dp[u];
			cout << ans << endl;
		}
	}
}

int main() {
	solve();
}

在这里插入图片描述


思路II

对边权的修改操作可用引用的方式完成,用vector<pair<int, int&>>存图,注意用emplace_back.

如下代码无需快读快写能在牛客上通过,但需快读快写才能在CF上通过.

代码II -> 2021CCPC江西省赛题解-I(vector引用+换根DP)

namespace FastIO {
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)  // 重写getchar()
#define pc(ch) (p - buf2 == SIZE ? fwrite(buf2, 1, SIZE, stdout), p = buf2, *p++ = ch : *p++ = ch)  // 重写putchar()
	char buf[1 << 23], * p1 = buf, * p2 = buf;

	template<typename T>
	void read(T& x) {  // 数字快读
		x = 0;
		T sgn = 1;
		char ch = gc();
		while (ch < '0' || ch > '9') {
			if (ch == '-') sgn = -1;
			ch = gc();
		}
		while (ch >= '0' && ch <= '9') {
			x = (((x << 2) + x) << 1) + (ch & 15);
			ch = gc();
		}
		x *= sgn;
	}

	const int SIZE = 1 << 21;
	int stk[40], top;
	char buf1[SIZE], buf2[SIZE], * p = buf2, * s = buf1, * t = buf1;

	template<typename T>
	void print_number(T x) {
		p = buf2;  // 复位指针p

		if (!x) {
			pc('0');
			return;
		}

		top = 0;  // 栈顶指针
		if (x < 0) {
			pc('-');
			x = ~x + 1;  // 取相反数
		}

		do {
			stk[top++] = x % 10;
			x /= 10;
		} while (x);
		while (top) pc(stk[--top] + 48);
	}

	template<typename T>
	void write(T x) {  // 数字快写
		print_number(x);
		fwrite(buf2, 1, p - buf2, stdout);
	}
};
using namespace FastIO;

const int MAXN = 1e5 + 5, MAXM = MAXN << 1;
int n, q;
int a[MAXN];  // 每个同学独立完成作业的时间
int w[MAXN];  // 边权
vector<pair<int, int&>> edges[MAXN];
int dp[MAXN];  // dp[u]表示只考虑节点u的子树的前提下同学u完成作业的最早时间

void dfs1(int u, int fa) {  // 当前节点、前驱节点
	dp[u] = a[u];  // 初始化为独立完成作业的时间
	for (auto& [v, w] : edges[u]) {
		if (v == fa) continue;

		dfs1(v, u);  // 递归求子树的信息
		dp[u] = min(dp[u], dp[v] + w);
	}
}

void dfs2(int u, int fa) {  // 当前节点、前驱节点
	for (auto& [v, w] : edges[u]) {
		if (v == fa) continue;

		dp[v] = min(dp[v], dp[u] + w);  // 换根
		dfs2(v, u);  // 递归求子树的信息
	}
}

void solve() {
	read(n), read(q);
	for (int i = 1; i <= n; i++) read(a[i]);
	for (int i = 1; i < n; i++) {
		int u, v; read(u), read(v), read(w[i]);
		edges[u].emplace_back(v,w[i]), edges[v].emplace_back(u,w[i]);
	}

	while (q--) {
		int op; read(op);
		if (op == 1) {
			int i, x; read(i), read(x);
			a[i] = x;
		}
		else if (op == 2) {
			int i, x; read(i), read(x);
			w[i] = x;
		}
		else {
			dfs1(1, -1);  // 从1号节点开始搜,无前驱节点
			dfs2(1, -1);  // 从1号节点开始搜,无前驱节点

			int ans = 0;
			for (int u = 1; u <= n; u++) ans ^= dp[u];
			write(ans), putchar('\n');
		}
	}
}

int main() {
	solve();
}

在这里插入图片描述



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值