友谊赛题解

A-Rainyrabbit 爱邮递

算法 1

每次重建快递站后直接深搜一遍累加贡献就好了,然后 O ( 1 ) \mathcal{O}(1) O(1) 回答。

你可以获得 10pts 的好成绩。

算法 2

菊花图,这个很简单啊。

如果加的是花心,相当于其他城市都加 1 1 1,否则就是花心的城市加 1 1 1,其他城市加 2 2 2

结合算法 1 可以获得 30pts 的好成绩。

算法 3

一条链的话也是送的,直接拆拆贡献用棵线段树维护即可。

结合算法 1,2 可以获得 60pts 的好成绩。

算法 4

做法 1

考虑树怎么做,很套路的可以将 d i s ( x , y ) dis(x,y) dis(x,y) 拆成 d e p x + d e p y − 2 d e p lca ⁡ ( x , y ) dep_x+dep_y-2dep_{\operatorname{lca}(x,y)} depx+depy2deplca(x,y),但是拆了还是不好做,修改最坏还是 O ( n ) \mathcal{O(n)} O(n) 的。

发现 d e p lca ⁡ ( x , y ) dep_{\operatorname{lca}(x,y)} deplca(x,y) 这个玩意可以借助 [LNOI2014]LCA 的思想,将树根 1 1 1 x x x 上的边都加上一次这条边的权值,然后节点 y y y 往上爬,途中边排边累加该边的贡献,爬到根后就是 d e p lca ⁡ ( x , y ) dep_{\operatorname{lca}(x,y)} deplca(x,y) 的值。

于是便有了一个新的算法,对于重建快递站到 u u u,直接将 u u u 往上爬,并更新每条边新的贡献,查询的话就跟刚才一样的往上爬就好了。

暴力往上爬的话可以过数据随机的点,然后结合算法 1,2,3 就能获得 80pts 的好成绩。

正解直接用树链剖分优化即可。

时间复杂度 O ( n log ⁡ 2 n ) \mathcal{O}(n\log^2 n) O(nlog2n)

#include <cstdio>
#include <cstring>
#include <queue>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define int long long
int n, m, totdep, tot, a[200005], Dep[200005], fa[200005], size[200005], dep[200005], son[200005],
    seg[200005], rev[200005], top[200005];
struct node {
    int to, w;
};
vector<node> G[200005];
void dfs1(int u, int f) {
    size[u] = 1, dep[u] = dep[f] + 1, fa[u] = f;
    for (int i = 0; i < G[u].size(); i++) {
        int v = G[u][i].to;
        if (v == f)
            continue;
        Dep[v] = Dep[u] + G[u][i].w;
        a[v] = G[u][i].w;
        dfs1(v, u);
        size[u] += size[v];
        if (size[v] > size[son[u]])
            son[u] = v;
    }
}
void dfs2(int u, int f) {
    if (son[u]) {
        seg[son[u]] = ++seg[0];
        top[son[u]] = top[u];
        rev[seg[0]] = son[u];
        dfs2(son[u], u);
    }
    for (int i = 0; i < G[u].size(); i++) {
        int v = G[u][i].to;
        if (v == f)
            continue;
        if (!top[v]) {
            seg[v] = ++seg[0];
            top[v] = v;
            rev[seg[0]] = v;
            dfs2(v, u);
        }
    }
}
struct Segment_Tree {
    struct node {
        LL xsum, sum, lazy;
    };
    node tree[800005];
#define lson(x) x << 1
#define rson(x) x << 1 | 1
    void build(int x, int l, int r) {
        if (l == r) {
            tree[x].sum = a[rev[l]];
            return;
        }
        int mid = (l + r) >> 1;
        build(lson(x), l, mid);
        build(rson(x), mid + 1, r);
        tree[x].sum = tree[lson(x)].sum + tree[rson(x)].sum;
        tree[x].xsum = tree[lson(x)].xsum + tree[rson(x)].xsum;
    }
    void push_down(int x) {
        if (tree[x].lazy) {
            tree[lson(x)].lazy += tree[x].lazy;
            tree[rson(x)].lazy += tree[x].lazy;
            tree[lson(x)].xsum += tree[x].lazy * tree[lson(x)].sum;
            tree[rson(x)].xsum += tree[x].lazy * tree[rson(x)].sum;
            tree[x].lazy = 0;
        }
    }
    void add(int x, int l, int r, int L, int R) {
        if (L <= l && r <= R) {
            tree[x].lazy++;
            tree[x].xsum += tree[x].sum;
            return;
        }
        push_down(x);
        int mid = (l + r) >> 1;
        if (L <= mid)
            add(lson(x), l, mid, L, R);
        if (mid + 1 <= R)
            add(rson(x), mid + 1, r, L, R);
        tree[x].xsum = tree[lson(x)].xsum + tree[rson(x)].xsum;
    }
    LL query(int x, int l, int r, int L, int R) {
        if (L <= l && r <= R)
            return tree[x].xsum;
        LL ans = 0;
        int mid = (l + r) >> 1;
        push_down(x);
        if (L <= mid)
            ans += query(lson(x), l, mid, L, R);
        if (mid + 1 <= R)
            ans += query(rson(x), mid + 1, r, L, R);
        return ans;
    }
} T;
void change(int u) {
    while (u) {
        int fu = top[u];
        T.add(1, 1, n, seg[fu], seg[u]);
        u = fa[fu];
    }
}
LL query(int u) {
    LL ans = 0;
    while (u) {
        int fu = top[u];
        ans += T.query(1, 1, n, seg[fu], seg[u]);
        u = fa[fu];
    }
    return ans;
}
signed main() {
    scanf("%lld %lld", &n, &m);
    for (int i = 1; i < n; i++) {
        int u, v, w;
        scanf("%lld %lld %lld", &u, &v, &w);
        G[u].push_back(node{ v, w });
        G[v].push_back(node{ u, w });
    }
    dfs1(1, 0);
    seg[0] = 1, seg[1] = 1, top[1] = rev[1] = 1;
    dfs2(1, 0);
    T.build(1, 1, seg[0]);
    while (m--) {
        int opt, u;
        scanf("%lld %lld", &opt, &u);
        if (opt == 1)
            change(u), totdep += Dep[u], tot++;
        else
            printf("%lld\n", totdep + 1ll * tot * Dep[u] - 2 * query(u));
    }
    return 0;
}

做法 2

来自巨佬 WY 的做法,cdq 分治+虚树。

B-Rainyrabbit 爱回文

很遗憾,因为数据过水,导致有 5 人 AC,但是本质上只有 3 人写的正解,其他的时间复杂度都是错的。

多次询问一个字符串 s s s,问构造若干字符串 t 1 , t 2 , ⋯   , t k t_1,t_2,\cdots ,t_k t1,t2,,tk 使得 s = t 1 t 2 ⋯ t k s=t_1t_2\cdots t_k s=t1t2tk 并且 ∀ i ∈ [ 1 , k ] , t i ≥ 2 \forall i \in [1,k],t_i \geq 2 i[1,k],ti2 并且是回文串的方式是否存在。 T ≤ 10 , ∣ s ∣ ≤ 1 0 6 T \leq 10,|s| \leq 10^6 T10,s106

首先每次选最大的回文串肯定是错的。具体看这个串 ababallllllab

先求出 f f f f i f_i fi 为以字符 i i i 为开头的最短回文串长度。

考虑这个辅助数组的作用。假设我们的合法划分方案,在这里的划分出来的回文串长度是 d d d,那么这个 f i f_i fi d d d 的关系是什么呢?

首先,显然 f i ≤ d f_i \leq d fid。分类讨论:

  • f i = d f_i = d fi=d:那没事儿了。
  • f i < d 2 f_i < \dfrac{d}{2} fi<2d:显然我们可以将这个字符串 d d d 看成三个回文串,也就是一段 f i f_i fi,以最后一个字符为中心对称过去,或者是再恰一个字符再对称,或者是直接对称过去;
  • 否则这种情况是不可能的,因为这样我们能够找到更小的前缀回文串。可以自行理解。注意这个更小的前缀回文串可能是一个字母,需要特判。

综上,以 i i i 开头的回文串长度的选择只有四个: f i , 2 f i − 1 , 2 f i + 1 , 2 f i f_i,2f_i-1,2f_i+1,2f_i fi,2fi1,2fi+1,2fi。可以证明其可以涵盖所有情况。剩下的工作就是做个 dp。

考虑求出这个 f f f 数组。有三种做法,时间复杂度分别是 O ( n log ⁡ n ) , O ( n α ( n ) ) O(n \log n),O(n \alpha(n)) O(nlogn),O(nα(n))。标程给的是 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的做法。 O ( n α ( n ) ) O(n \alpha(n)) O(nα(n)) 做法由 @chihik 提出。

理论上来说有 O ( n ) O(n) O(n) 的做法,但是我太菜没听懂 @crashed 再说啥。

O ( n log ⁡ n ) O(n \log n) O(nlogn)

考虑对于每一个字符,求出以它为中心(中心可以是和下一个字符结合也可以是一个字符,这个是两类)的最长回文串,在其左端点打上标记。标记记录三个值,表示其起始位置,有效长度及中心是和下一个字符结合还是一个字符。做法是 hash 二分。你高兴也可以 manacher。

这个东西的意义是,我们选择的 f i f_i fi,一定是离有效范围内结束最近的那个位置的距离。于是处理三个东西,可以发现这个东西可以全部拍进 set 里面选最小的且合法的。具体处理方法看代码。

代码有个小补丁,就是要处理两个相邻字符相同的情况。这个时候直接把 f i f_i fi 设为 2 2 2 即可。

/*
LL         JJ  SSSS    ii  ssss     SSSS BBBB
LL         JJ S           s        S     B   B
LL         JJ  SSS     ii  sss      SSS  BBBB
LL     JJ  JJ     S    ii     s        S B   B
LLLLLL  JJJJ  SSSS     ii ssss     SSSS  BBBB

 SSSS BBBB     ii  ssss    LL         JJ  SSSS
S     B   B       s        LL         JJ S
 SSS  BBBB     ii  sss     LL         JJ  SSS
    S B   B    ii     s    LL     JJ  JJ     S
SSSS  BBBB     ii ssss     LLLLLL  JJJJ  SSSS
*/
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const ULL base=10086001;
ULL lhsh[3000005],rhsh[3000005],pw[3000005];
char s[3000005];
ULL getHashL(int l,int r){return lhsh[r]-lhsh[l-1]*pw[r-l+1];}
ULL getHashR(int l,int r){return rhsh[l]-rhsh[r+1]*pw[r-l+1];}
int n,f[3000005];
bool isPalindrome(int l,int r){return getHashL(l,r)==getHashR(l,r);}
struct pldrome{
	int pos,len,type;
	pldrome(int R=0,int L=0,int T=0){pos=R,len=L,type=T;}
	bool operator != (const pldrome ano) const {return pos!=ano.pos || len!=ano.len || type!=ano.type;}
	bool operator < (const pldrome ano) const {
		int ps=pos,ln=len,tp=type,pst=ano.pos,lnt=ano.len,tpt=ano.type;
		if(tp==0)	--ln;
		if(tpt==0)	--lnt;
		int ed1=ps+ln-1,ed2=pst+lnt-1;
		return ed1<ed2;
	}
};
vector<pldrome> G[3000005];
bool dp[3000005];
int main(){ 
	pw[0]=1;
	for(int i=1;i<=3000000;++i)	pw[i]=pw[i-1]*base; 
	int T;
	scanf("%d",&T);
	dp[0]=true;
	while(T-->0)
	{
		scanf("%s",s+1);
		n=strlen(s+1);
		for(int i=1;i<=n;++i)	lhsh[i]=lhsh[i-1]*base+ULL(s[i]),f[i]=0;
		for(int i=n;i>=1;--i)	rhsh[i]=rhsh[i+1]*base+ULL(s[i]),dp[i]=false;
		for(int i=1;i<=n;++i)
		{
			if(i==1)
			{
				if(isPalindrome(1,2))	G[1].push_back(pldrome(1,1,1));
			}
			else if(i==n-1)
			{
				if(isPalindrome(n-1,n))	G[n-1].push_back(pldrome(n-1,1,1));
				if(isPalindrome(n-2,n))	G[n-2].push_back(pldrome(n-2,2,0));
			}
			else if(i==n)	continue;
			else
			{
				//i is medium
				int l=0,r=min(i-1,n-i),ans=0;
				while(l<=r)
				{
					int mid=(l+r)>>1;
					if(isPalindrome(i-mid,i+mid))	ans=mid,l=mid+1;
					else	r=mid-1;
				}
				if(ans)	G[i-ans].push_back(pldrome(i-ans,ans+1,0));
				l=0,r=min(i-1,n-i-1),ans=0;
				while(l<=r)
				{
					int mid=(l+r)>>1;
					if(isPalindrome(i-mid,i+1+mid))	ans=mid,l=mid+1;
					else	r=mid-1;
				}
				if(ans)	G[i-ans].push_back(pldrome(i-ans,ans+1,1));
				else
				{
					if(isPalindrome(i,i+1))	G[i].push_back(pldrome(i,1,1));
				}
			}
		}
		set<pldrome> S;
		for(int i=1;i<=n;++i)
		{
			while(!G[i].empty())	S.insert(G[i].back()),G[i].pop_back();
			while(!S.empty())
			{
				pldrome st=*S.begin();
				int ps=st.pos,ln=st.len,tp=st.type;
				if(tp==0)	--ln;
				int ed=ps+ln-1;
				if(ed<i)
				{
					S.erase(S.begin());
					continue;
				}
				if(tp==0)
				{
					++ed;
					int edTrue=2*ed-i;
					f[i]=edTrue-i+1;
				}
				else
				{
					int edTrue=2*ed-i+1;
					f[i]=edTrue-i+1;
				}
				break;
			}
			if(S.empty())	f[i]=-1;
			if(i<=n-1 && isPalindrome(i,i+1))	f[i]=2;	
		}
		for(int i=1;i<=n;++i)
		{
			if(f[i]==-1 || !dp[i-1])	continue;
			dp[i+f[i]-1]=true;
			int sp=2*f[i]-1;
			if(isPalindrome(i,i+sp-1))	dp[i+sp-1]=true;
			sp+=2;
			if(isPalindrome(i,i+sp-1))	dp[i+sp-1]=true;
			--sp;
			if(isPalindrome(i,i+sp-1))	dp[i+sp-1]=true;
		}
		puts(dp[n]?"YeseY":"NoN");
	}
	return 0;
}

O ( n α ( n ) ) O(n \alpha (n)) O(nα(n)) By @chihik

考虑回文自动机。一样的思路,求出以某位为起点的最长回文串。暴力回跳。显然 T,于是并查集优化即可。

O ( n ) O(n) O(n) By crashed / LY(初二)

本意是选联赛范围考察。但是因为大家都会回文自动机所以被踩了。反正其实我也不会。

具体做法是链上排一次序(基排 By crashed)。

C-Rainyrabbit 爱求和

菜鸡出的垃圾套路题。/kk

算法 1

测试点 1 1 1 随便手玩。

期望得分 4 分。

算法 2

测试点 2 ∼ 3 2\sim 3 23 按照题意模拟。

期望得分 12 分。

算法 3

测试点 4 ∼ 5 4\sim 5 45 先在外面直接算然后直接回答。

期望得分 20 分。


#include<cstdio>
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
#define LL long long
const int mod=998244353;
int dec(int x, int y) { return x >= y ? x - y : x + mod - y; }
int mul(int x, int y) { return 1ll * x * y % mod; }
int add(int x, int y) {
    if (x + y >= mod)
        return x + y - mod;
    return x + y;
}
int qkpow(LL a, LL b) {
    int res = 1;
    for (; b; b >>= 1, a = mul(a, a))
        if (b & 1)
            res = mul(res, a);
    return res;
}
int t,mem[105][105][105];
LL a,b,c;
LL lcm(LL a,LL b){
	return a/__gcd(a,b)*b;
}
LL sigma(LL n,LL k){
	LL ans=0;
	for(int i=1;i<=n;i++){
		if(n%i==0)ans=add(ans,qkpow(i,k));
	}
	return ans;
}
LL f(LL n,LL m,LL k){
	LL ans=0;
	for(LL i=m;i<=n;i+=m)ans=add(ans,sigma(i,k));
	return ans;
}
signed main(){
	cin>>t>>c;
	for(int i=1;i<=100;i++)
		for(int j=1;j<=100;j++)
			for(int k=0;k<=c;k++)
				mem[i][j][k]=k==0?f(i,j,0):add(mem[i][j][k-1],f(i,j,k));
	for(int i=1;i<=100;i++){
		for(int j=1;j<=100;j++){
			mem[i][j][c]=add(mem[i][j][c],add(mem[i][j-1][c],mem[i-1][j][c]));
			mem[i][j][c]=dec(mem[i][j][c],mem[i-1][j-1][c]);
		}
	}
	while(t--){
		cin>>a>>b;
		cout<<mem[a][b][c]<<endl;
	}
	return 0;
} 
/*
5 5
4 1 3 6 2
1 2 4
2 1 3
0 0 3 2
1 1 2
2 2 4
*/

算法 4

送分结束。

来推一推 f f f 函数。

f ( n , m , k ) = ∑ d = 1 n d k ⌊ n lcm ⁡ ( d , m ) ⌋ = ∑ d = 1 n d k ⌊ n d ⌊ m gcd ⁡ ( d , m ) ⌋ ⌋ f(n,m,k)=\sum_{d=1}^n d^k \lfloor\dfrac{n}{\operatorname{lcm}(d,m)}\rfloor=\sum_{d=1}^n d^k \lfloor\dfrac{\dfrac{n}{d}}{\lfloor\dfrac{m}{\gcd(d,m)}\rfloor}\rfloor f(n,m,k)=d=1ndklcm(d,m)n=d=1ndkgcd(d,m)mdn

这个东西是有意义的, ∑ d = 1 n d k ⌊ n d ⌊ m gcd ⁡ ( d , m ) ⌋ ⌋ = ∑ i = 1 ⌊ n d ⌋ [ i d ∣ m ] \sum_{d=1}^n d^k \lfloor\dfrac{\dfrac{n}{d}}{\lfloor\dfrac{m}{\gcd(d,m)}\rfloor}\rfloor=\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}[id|m] d=1ndkgcd(d,m)mdn=i=1dn[idm]

然后拆回来

f ( n , m , k ) = ∑ m ∣ i n ∑ d ∣ i d k = ∑ m ∣ i n σ k ( i ) f(n,m,k)=\sum_{m|i}^n \sum_{d|i}d^k=\sum_{m|i}^n \sigma_k(i) f(n,m,k)=mindidk=minσk(i)

结论就推完了。

代回去

∑ k = 0 c ∑ i = 1 a ∑ j = 1 b ∑ j ∣ p i σ k ( p ) \sum_{k=0}^c\sum_{i=1}^a\sum_{j=1}^b\sum_{j|p}^i\sigma_k(p) k=0ci=1aj=1bjpiσk(p)

∑ k = 0 c ∑ p = 1 a σ k ( p ) ( a − p + 1 ) ∑ j = 1 b [ j ∣ p ] \sum_{k=0}^c\sum_{p=1}^a\sigma_k(p)(a-p+1)\sum_{j=1}^b[j|p] k=0cp=1aσk(p)(ap+1)j=1b[jp]

σ k ( p ) \sigma_k(p) σk(p) 那里可以直接把 σ 0 ( p ) , σ 1 ( p ) . . . , σ c ( p ) \sigma_0(p),\sigma_1(p)...,\sigma_c(p) σ0(p),σ1(p)...,σc(p) 都累加到一坨,然后直接 O ( n 2 ) \mathcal{O}(n^2) O(n2) 计算即可。

跟刚才一样的操作,先在外面都算出来再回答,可以获得 36pts 的好成绩。

算法 5

发现 b b b 的那个求和并不需要一个一个算,直接调和级数复杂度算就好了,然后就能获得 40 pts。

算法 6

c c c 太大了,推一推怎么算 ∑ i = 0 c σ i ( p ) \sum_{i=0}^c \sigma_i(p) i=0cσi(p)

∑ i = 0 c ∑ d ∣ p d i \sum_{i=0}^c \sum_{d|p}d^i i=0cdpdi

发现对于每一个因子的贡献都是一个等比数列。

∑ d ∣ p d c + 1 − 1 d − 1 \sum_{d|p}\dfrac{d^{c+1}-1}{d-1} dpd1dc+11

线性筛后调和级数算就好了,然后就能获得 68 pts 了。

算法 7

发现 b b b 超过 a a a 的范围以后是不会有任何贡献的,于是取个 min ⁡ \min min 算,至此就能获得 80 pts 的好成绩。

算法 8

这东西并不好多组询问, a , b a,b a,b 变来变去的很烦。

考虑离线,将每一个询问按照 b b b 排序。当 b = i b=i b=i 时能影响到的位置是 n i \dfrac{n}{i} in 个,直接树状数组修改即可。修改完后,直接查询就好了。

时间复杂度 O ( a ln ⁡ a + T log ⁡ T + min ⁡ ( a , b ) ln ⁡ min ⁡ ( a , b ) log ⁡ a + T log ⁡ a ) \mathcal{O}(a\ln a+T\log T+\min(a,b)\ln \min(a,b)\log a+T\log a) O(alna+TlogT+min(a,b)lnmin(a,b)loga+Tloga)

由于这东西并跑不满,跑 1 0 6 10^6 106 是跑的过的。代码及其好写。

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
const int mod = 998244353;
int dec(int x, int y) { return x >= y ? x - y : x + mod - y; }
int mul(int x, int y) { return 1ll * x * y % mod; }
int add(int x, int y) {
    if (x + y >= mod)
        return x + y - mod;
    return x + y;
}
int qkpow(LL a, LL b) {
    int res = 1;
    for (; b; b >>= 1, a = mul(a, a))
        if (b & 1)
            res = mul(res, a);
    return res;
}
int T, cnt, maxa, a[200005], prime[1000005], pw[1000005], inv[1000005], f[1000005], s[1000005], anos[1000005],
    ans[1000005];
bool vis[1000005];
LL b[200005], c;
struct node {
    int id, a;
};
vector<node> G[1000005];
struct BIT {
    int tree[1000005];
    void Add(int x, int val) {
        while (x <= maxa) {
            tree[x] = add(tree[x], val);
            x += x & (-x);
        }
    }
    int query(int x) {
        int Ans = 0;
        while (x) {
            Ans = add(Ans, tree[x]);
            x -= x & (-x);
        }
        return Ans;
    }
} T1, T2;
void seive() {
    pw[1] = 1;
    for (int i = 2; i <= 1000000; i++) {
        if (!vis[i])
            prime[++cnt] = i, pw[i] = qkpow(i, c + 1);
        for (int j = 1; j <= cnt && i * prime[j] <= 1000000; j++) {
            vis[i * prime[j]] = 1;
            pw[i * prime[j]] = mul(pw[i], pw[prime[j]]);
            if (i % prime[j] == 0)
                break;
        }
    }
    inv[1] = 1;
    for (int i = 2; i <= 1000000; i++) inv[i] = (LL)(mod - mod / i) * inv[mod % i] % mod;
    f[1] = add(c % mod, 1);
    for (int i = 2; i <= 1000000; i++) f[i] = mul(dec(pw[i], 1), inv[i - 1]);
    for (int i = 1; i <= 1000000; i++)
        for (int j = i; j <= 1000000; j += i) s[j] = add(s[j], f[i]);
    for (int i = 1; i <= 1000000; i++) anos[i] = mul(s[i], i);
}
signed main() {
    scanf("%d %lld", &T, &c);
    seive();
    for (int i = 1; i <= T; i++)
        scanf("%d %lld", &a[i], &b[i]), maxa = max(maxa, a[i]), b[i] = min(b[i], 1ll * a[i]);
    for (int i = 1; i <= T; i++) G[b[i]].push_back(node{ i, a[i] });
    for (int i = 1; i <= maxa; i++) {
        for (int j = i; j <= maxa; j += i) T1.Add(j, s[j]), T2.Add(j, anos[j]);
        for (int j = 0; j < G[i].size(); j++) {
            node now = G[i][j];
            ans[now.id] = dec(mul(now.a + 1, T1.query(now.a)), T2.query(now.a));
        }
    }
    for (int i = 1; i <= T; i++) printf("%d\n", ans[i]);
    return 0;
}

D-Rainyrabbit 爱染色

本场比赛的防 AK 题。其实思维难度不算太大,只是极其难写。

算法 1

爆搜即可。可以获得 10 pts。

算法 2

考虑 O ( n m ) \mathcal{O}(nm) O(nm) 的做法。

考虑贪心,每次选择只由该关键点覆盖的节点个数最多的关键点拿来染色,选完后重新更新一下(因为选了一个关键点染色),重复以上步骤 m m m 次,期间更新一下答案就好了,容易证明得出的答案一定是最优的。

期望得分 40 pts。

算法 3

先来考虑优化链的情况。

发现每个关键点控制的范围都是一个连续的区间,用一棵线段树维护即可。

菊花图就更简单了,如果花心 d i ≥ 1 d_i\ge 1 di1 全部都能控制到,其他的点如果 d i = 1 d_i=1 di=1 就只能控制花心, d i = 2 d_i=2 di=2 就能控制所有的点。随便更新一下就好了。

结合算法 2 期望得分 60pts。

算法 4

发现复杂度瓶颈在于怎么快速找到只由该关键点覆盖的节点个数最多的关键点及其更新。

先来看看 d i ∈ 0 , 1 d_i\in{0,1} di0,1 的情况,因为每个关键点能覆盖的节点是散开的,不好处理。于是考虑将树从上到下每一层从左到右编个号,然后你会惊奇的发现对于 d i = 1 d_i=1 di=1 的关键点能染到的点可以看成一个连续的区间和该关键点父亲的编号。于是可以想到用线段树维护,用每一个点的编号建树,但是发现不太好维护,因为要知道是哪一个关键点,而不是最多的点数,于是想到在线段树的每一个节点上都开一个 set 维护这个区间编号的节点有哪些关键点能够染到,找到后删除的话就直接 erase 就好了。现在的问题,怎么找????考虑直接从根节点往下搜,直接暴力找肯定会 T,考虑对于线段树上的每一个节点都维护一个区间最小值,表示这个区间至少有多少个关键点覆盖,如果关键点个数大于 1 1 1 或者已经被选过的关键点染过就不搜了,否则就拿出 set 里的关键点,往下搜,搜到 l = r l=r l=r 时就将该关键点的 c n t cnt cnt 加一,放进堆里就好了。

关于找最小字典序的序列也很简单,每次选个数最多编号最小的关键点就好了,然后跟上面一样跑。

期望得分 70pts。

算法 5

现在来玩 d i = 2 d_i=2 di=2 的情况,发现也可以拆分成几个编号连续的区间。该关键点的下面两层,它的父亲的父亲,它的父亲的父亲下面那一层。然后跟上面那样一样做就行。

时间复杂度 O ( m log ⁡ 2 n ∼ m log ⁡ 3 n ) \mathcal{O}(m\log^2 n\sim m\log^3n ) O(mlog2nmlog3n)

#include <set>
#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, Cnt, ans = 1e9;
int im_node[3][200005], cnt[200005], fa[200005], L1[200005], R1[200005], L2[200005], R2[200005], dep[200005], dfn[200005];
bool isdead[200005];

#define pi pair<int, int>
#define lson(x) x << 1
#define rson(x) x << 1 | 1

struct node {
    int minn, lazy;
} tree[400005];

set<int>::iterator it;
set<int> cover[400005];

vector<int> G[200005], g[200005];

priority_queue<pi> Q1;
priority_queue<int> Q2;

void dfs1(int u, int f) {
    dep[u] = dep[f] + 1, fa[u] = f;
    g[dep[u]].push_back(u);
    for (int i = 0; i < G[u].size(); i++) {
        int v = G[u][i];
        if (v == f)
            continue;
        dfs1(v, u);
    }
}

void dfs2(int u, int f) {
    L1[u] = L2[u] = 1e9, R1[u] = R2[u] = 0;
    for (int i = 0; i < G[u].size(); i++) {
        int v = G[u][i];
        if (v == f)
            continue;
        dfs2(v, u);
        L1[u] = min(L1[u], dfn[v]);
        L2[u] = min(L2[u], L1[v]);
        R1[u] = max(R1[u], dfn[v]);
        R2[u] = max(R2[u], R1[v]);
    }
}

void rebuild(int x, int l, int r) {
    tree[x].lazy = tree[x].minn = 0;
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    rebuild(lson(x), l, mid);
    rebuild(rson(x), mid + 1, r);
}

void push_down(int x) {
    if (tree[x].lazy) {
        tree[lson(x)].lazy += tree[x].lazy;
        tree[rson(x)].lazy += tree[x].lazy;
        tree[lson(x)].minn += tree[x].lazy;
        tree[rson(x)].minn += tree[x].lazy;
        tree[x].lazy = 0;
    }
}

void Updata(int x, int l, int r, int u) {
    if (tree[x].minn > 1)
        return;
    if (cover[x].size())
        u = *cover[x].begin();
    if (l == r) {
        if (tree[x].minn == 0)
            tree[x].minn = 1e9;
        else if (tree[x].minn == 1) {
            cnt[u]++;
            Q1.push(make_pair(cnt[u], u));
            tree[x].minn = 1e9;
        }
        return;
    }
    push_down(x);
    int mid = (l + r) >> 1;
    Updata(lson(x), l, mid, u);
    Updata(rson(x), mid + 1, r, u);
    tree[x].minn = min(tree[lson(x)].minn, tree[rson(x)].minn);
}

void sub(int x, int l, int r, int L, int R, int u) {
    if (L <= l && r <= R) {
        tree[x].lazy--, tree[x].minn--;
        it = cover[x].find(u);
        cover[x].erase(it);
        return;
    }
    push_down(x);
    int mid = (l + r) >> 1;
    if (L <= mid)
        sub(lson(x), l, mid, L, R, u);
    if (mid + 1 <= R)
        sub(rson(x), mid + 1, r, L, R, u);
    tree[x].minn = min(tree[lson(x)].minn, tree[rson(x)].minn) + tree[x].lazy;
}

void add(int x, int l, int r, int L, int R, int u) {
    if (L <= l && r <= R) {
        tree[x].lazy++, tree[x].minn++;
        cover[x].insert(u);
        return;
    }
    push_down(x);
    int mid = (l + r) >> 1;
    if (L <= mid)
        add(lson(x), l, mid, L, R, u);
    if (mid + 1 <= R)
        add(rson(x), mid + 1, r, L, R, u);
    tree[x].minn = min(tree[lson(x)].minn, tree[rson(x)].minn);
}

void updata(int opt, int u, int l, int r) {
    if (l > r)
        return;
    if (opt == 1)
        add(1, 1, n, l, r, u);
    else
        sub(1, 1, n, l, r, u);
}

void add_or_sub_im_node(int x, int opt) {
    int u = im_node[0][x], d = im_node[1][x];
    if (d == 0)
        updata(opt, x, dfn[u], dfn[u]);
    else if (d == 1) {
        updata(opt, x, L1[u], R1[u]);
        updata(opt, x, dfn[u], dfn[u]);
        if (fa[u])
            updata(opt, x, dfn[fa[u]], dfn[fa[u]]);
    } else {
        updata(opt, x, L1[u], R1[u]);
        updata(opt, x, L2[u], R2[u]);
        if (fa[u]) {
            int v = fa[u];
            updata(opt, x, L1[v], R1[v]);
            updata(opt, x, dfn[v], dfn[v]);
            if (fa[v])
                updata(opt, x, dfn[fa[v]], dfn[fa[v]]);
        } else
            updata(opt, x, dfn[u], dfn[u]);
    }
}

int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d %d", &u, &v);
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs1(1, 0);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < g[i].size(); j++) dfn[g[i][j]] = ++Cnt;
    dfs2(1, 0);
    for (int i = 1; i <= m; i++) {
        scanf("%d %d", &im_node[0][i], &im_node[1][i]);
        add_or_sub_im_node(i, 1);
    }
    for (int i = 1; i <= m; i++) Q1.push(make_pair(0, i));
    Updata(1, 1, n, -1);
    for (int i = 1; i <= m; i++) {
        while (isdead[Q1.top().second] || Q1.top().first != cnt[Q1.top().second]) Q1.pop();
        pi now = Q1.top();
        Q1.pop();
        ans = min(ans, now.first);
        isdead[now.second] = 1;
        add_or_sub_im_node(now.second, -1);
        Updata(1, 1, n, -1);
    }
    printf("%d\n", ans);
    rebuild(1, 1, n);
    while (!Q1.empty()) Q1.pop();
    for (int i = 1; i <= m; i++) isdead[i] = 0, cnt[i] = 0, add_or_sub_im_node(i, 1), Q1.push(make_pair(0, i));
    Updata(1, 1, n, -1);
    int choose = 0;
    for (int i = 1; i <= m; i++) {
        while (choose < m) {
            while (isdead[Q1.top().second] || Q1.top().first != cnt[Q1.top().second]) Q1.pop();
            if (Q1.top().first < ans)
                break;
            Q2.push(-Q1.top().second);
            isdead[Q1.top().second] = 1;
            Q1.pop();
            choose++;
        }
        add_or_sub_im_node(-Q2.top(), -1);
        Q2.pop();
        Updata(1, 1, n, -1);
    }
    for (int i = 1; i <= m; i++) printf("%d\n", cnt[i]);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值