contest1 题解

A. 序列分割

容易发现分割顺序并不影响答案。

因为你确定下来最终状态后,设每段的和分别为 \(x_1,x_2,\ldots x_m\),发现你这个分割实际上是统计了 \(\prod_{i < j} x_ix_j\)。(每次都是统计跨过某个点的所有对,然后递归)

所以我们可以设计一个 dp:\(f_{i,j}\) 表示分了 \(i\) 段,第 \(i\) 次分的位置是 \(j\) 的答案。记前缀和为 \(s_i\),转移是:

\[\begin{aligned} f_{i,j} &= f_{i-1,k}+(s_j-s_k)(s_n-s_j)\\ &= s_ks_j+(f_{i-1,k}+s_ks_n)+s_js_n-s_j^2 \end{aligned} \]

考虑斜率优化。为了方便设 \(f_j = f_{i,j},g_j = f_{i-1,j}\)

那么相当于是对一堆一次函数 \(y=s_kx+g_k+s_ks_n\) 求在 \(x=s_j\) 的最大值。

求一次函数 \(y=a_ix+b_i\) 最大值,转化为 \(-xa_i+y=b_i\),相当于有若干个点 \((-a_i,b_i)\),每次带入一个斜率 \(x\) 求最大截距。

不难发现这要求维护的是上凸包(斜率递减的凸包)

然后发现这里插入的点横坐标是单调递减的,所以相当于要从右到左维护这个凸包。

查询的斜率是逐渐增大的,相当于也是从右到左,所以相当于在左边加,右边减,用双端队列维护即可。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 1e5 + 5;

int now,a[MAXN],n,k;
LL sm[MAXN];
LL f[2][MAXN];

struct Node{
	LL x,y;
	Node(LL x=0,LL y=0) : x(x),y(y) {}
	
	inline Node operator - (const Node &t) const {
		return Node(x-t.x,y-t.y);
	}
	
	inline long double operator * (const Node &t) const {
		return 1.0*x*t.y-1.0*y*t.x;
	}
};
std::deque<Node> q;

inline LL calc(Node a,LL x){
	return -a.x*x+a.y;
}

int main(){
	scanf("%d%d",&n,&k);
	FOR(i,1,n) scanf("%d",a+i);
	FOR(i,1,n) sm[i] = sm[i-1]+a[i];
	// f[i][j] = f[k][j-1]+(sm[i]-sm[k])*(sm[n]-sm[i])
	FOR(i,1,n) f[now][i] = sm[i]*(sm[n]-sm[i]);
	FOR(j,2,k){
		CLR(f[now^1],0);q.clear();
		FOR(i,0,j-1){
			Node x(-sm[i],f[now][i]-sm[i]*sm[n]);
			while(q.size() >= 2 && (q.back()-q[(int)q.size()-2])*(x-q.back()) <= 0.0) q.pop_back();
			q.pb(x);
		}
		FOR(i,j,n){
			Node x(-sm[i],f[now][i]-sm[i]*sm[n]);
			while(q.size() >= 2 && (q.back()-q[(int)q.size()-2])*(x-q.back()) <= 0.0) q.pop_back();
			q.pb(x);
			while(q.size() >= 2 && calc(q[0],sm[i]) <= calc(q[1],sm[i])) q.pop_front();
			f[now^1][i] = calc(q[0],sm[i])+sm[i]*sm[n]-sm[i]*sm[i];
		}
		now ^= 1;
	}
	LL ans = 0;
	FOR(i,k,n) ans = std::max(ans,f[now][i]);
	printf("%lld\n",ans);
	return 0;
}

B. 某位歌姬的故事

做过类似的题。。

先思考 \(m_i=2\) 的部分分:一个暴力的想法是设 \(f_{i,j}\) 表示考虑了前 \(i\) 个位置,最后一次填的最大值位置在 \(j\),可以预处理出 \(lim_i\) 表示 \(i\) 左边一直不填,直到第一个必须要填的位置。转移直接枚举这一个位置是否填就行了。

对于 \(m_i\) 不同的部分分,首先很好判无解:如果有一个 \(m_i\) 大的区间被所有比它小的区间的并包含了,就无解。

否则我们就可以分开考虑每个权值:每次只考虑所有还没有被考虑过的位置,类似搞个 dp 即可。

但是 \(n\) 很大,所以我们要离散化。离散化后的一个点对应原来的一个左开右闭的区间,记录一下每个点对应的区间的长度就可以很好转移了。

具体实现分开每个权值的时候,只需要在离散化的点上用过的打标记,每次暴力查就行了。时间复杂度 \(O(Tq^2)\)

这个题其实可以优化:

首先查询当前权值对应的点的时候可以用并查集维护下一个位置,这样总复杂度就是 \(O(q \log q)\)

然后那个 dp 部分有一个经典的做法,观察 dp 转移:

\[\begin{aligned} f_{i,j}*gx_1 \to f_{i+1,j}\\ f_{i,j}*gx_2 \to f_{i+1,i+1} \end{aligned} \]

相当于每次转移分两种:首先是整体乘了 \(gx_1\),然后是所有合法的位置的和 \(\times gx_2\),转移到 \(f_{i+1,i+1}\)

合法的位置是单调不降的,可以拿个指针维护合法的位置,每次扫掉不合法的地方,然后用线段树或者打全局标记维护整体乘法即可。这一部分时间复杂度 \(O(q)\)

所以总复杂度可以做到优秀的 \(O(Tq \log q)\)。(那个 \(\log\) 来自于并查集)

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

int n,q,A;
const int ha = 998244353;
const int MAXN = 2000+5;

struct Node{
	int l,r,mx;	
}a[MAXN];

std::vector<int> S,T;
int lim[MAXN],len[MAXN];

inline int qpow(int a,int n=ha-2){
	int res = 1;
	while(n){
		if(n & 1) res = 1ll*res*a%ha;
		a = 1ll*a*a%ha;
		n >>= 1;
	}
	return res;
}

template <typename T>
inline void add(T &x,int y){
	x += y;if(x >= ha) x -= ha;
}

int N,M;
int lst[MAXN];
int f[MAXN][MAXN];
// 前i个点 上一次最大值放在j

inline int work(int c){
	std::vector<int> ps;ps.pb(-1);
	FOR(i,1,N){
		if(lim[i] == c) ps.pb(i);
	}
	CLR(lst,0);
	FOR(i,1,q){
		if(a[i].mx == c){
			int L = std::lower_bound(all(ps),a[i].l)-ps.begin();
			int R = std::upper_bound(all(ps),a[i].r)-ps.begin()-1;
			if(L > R) return -1;
			lst[R] = std::max(lst[R],L);
		}
	}
	int n = (int)ps.size()-1,A = T[c-1];
	FOR(i,1,n) lst[i] = std::max(lst[i],lst[i-1]);
	if(!n) return 1;
	// 处理出每个端点 这里不放哪里能放
	// FOR(i,1,n) DEBUG(lst[i]);
	FOR(i,0,n) FOR(j,0,n) f[i][j] = 0;f[0][0] = 1;
	FOR(i,0,n-1){
		FOR(j,0,i){
			if(!f[i][j]) continue;
			if(j < lst[i]) continue;
			int l = len[ps[i+1]];
			int gx1 = qpow(A-1,l),gx2 = (qpow(A,l)+ha-gx1)%ha;
			add(f[i+1][j],1ll*f[i][j]*gx1%ha);
			add(f[i+1][i+1],1ll*f[i][j]*gx2%ha);
		}
	}
	// DEBUG(f[2][1]);
	LL ans = 0;
	FOR(i,lst[n],n) add(ans,f[n][i]);
	return ans;
}

inline void Solve(){
	scanf("%d%d%d",&n,&q,&A);S.clear();T.clear();
	FOR(i,1,q){
		scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].mx);
		S.pb(a[i].l-1);S.pb(a[i].r);T.pb(a[i].mx);
	}
	S.pb(n);
	std::sort(all(S));std::sort(all(T));
	S.erase(std::unique(all(S)),S.end());
	T.erase(std::unique(all(T)),T.end());
	N = S.size();M = T.size();
	FOR(i,1,N) lim[i] = 1e9,len[i] = S[i-1]-(i==1?0:S[i-2]);
	S.erase(std::unique(all(S)),S.end());
	T.erase(std::unique(all(T)),T.end());
	FOR(i,1,q){
		a[i].l = std::lower_bound(all(S),a[i].l)-S.begin()+1;
		a[i].r = std::lower_bound(all(S),a[i].r)-S.begin()+1;
		a[i].mx = std::lower_bound(all(T),a[i].mx)-T.begin()+1;
		FOR(j,a[i].l,a[i].r) lim[j] = std::min(lim[j],a[i].mx);
	}
	int ans = 1;
	FOR(i,1,M){
		int t = work(i);
		if(t == -1){
			puts("0");
			return;
		}
		ans = 1ll*ans*t%ha;
	}
	FOR(i,1,N){
		if(lim[i] == 1e9){
			ans = 1ll*ans*qpow(A,len[i])%ha;
		}
	}
	printf("%d\n",ans);
}

int main(){
	int T;scanf("%d",&T);
	while(T--) Solve();
	return 0;
}

C. 榕树之心

我们观察 Sub3,相当于每次可以消掉两个不同子树的点,求最终是否能消光。

我们设 \(f_v\) 表示 \(v\) 子树最终可以消到只剩多少个点。那么我们肯定是尽量将重儿子子树消光。设每个点的重儿子是 \(mx_v\),转移分类讨论:

  • 如果 \(f_{mx_v}+1 \leq sz_v-1-sz_{mx_v}\),那么 \(f_v = (sz_v-1)\bmod 2\)
  • 如果 \(f_{mx_v}+1 > sz_v-1-sz_{mx_v}\),那么 \(f_v = f_{mx_v}+1-(sz_v-1-sz_{mx_v})\)

这里 \(+1\) 的原因是,在子树内是 \(f_v\),到了父亲节点后需要多经过一条边,也就是多找一个其他子树的消去。

(实际上这个状态理解为 \(v\) 最终能操作到距离自己最近的距离更好理解。。)

对于其他点 \(v\) 的询问,我们只要能先在点 \(v\) 的子树内预留一些节点最后选,最后将 \(v\) 拽过去就好了。

于是就等价于将 \(v \to 1\) 的路径上的点都合并成一个大点然后做这个问题。

实现可以再用一遍 dfs,每次往下递归的时候就求出当前这个点被并入大点后对最大值的贡献。注意到原始树上这个点的重儿子可能是接下来要遍历的点,所以也要记录一下次重儿子。

#include <bits/stdc++.h>

#define fi first
#define se second
#define db double
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 1e5 + 5;

struct Edge{
	int to,nxt;
}e[MAXN<<1];
int head[MAXN],cnt;

inline void add(int u,int v){
	e[++cnt] = (Edge){v,head[u]};head[u] = cnt;
	e[++cnt] = (Edge){u,head[v]};head[v] = cnt;
}

int W,n;

int sz[MAXN],f[MAXN];// 还剩下多少
int mx[MAXN],cmx[MAXN];

inline void dfs1(int v,int fa=0){
	sz[v] = 1;mx[v] = cmx[v] = 0;
	for(int i = head[v];i;i = e[i].nxt){
		if(e[i].to == fa) continue;
		dfs1(e[i].to,v);
		sz[v] += sz[e[i].to];
		if(sz[e[i].to] > sz[mx[v]]) cmx[v] = mx[v],mx[v] = e[i].to;
		else if(sz[e[i].to] > sz[cmx[v]]) cmx[v] = e[i].to;
	}
	// 子节点剩下i <=> v节点剩下i+1(要多走一条边)
	if(f[mx[v]]+1 <= sz[v]-1-sz[mx[v]]) f[v] = (sz[v]-1)&1;
	else f[v] = f[mx[v]]+1-(sz[v]-1-sz[mx[v]]);
}

int ans[MAXN];

inline void dfs2(int v,int fa=0,int dep=1,int p=0){// p: 并起来之后的最大子树
	int x = sz[p] > sz[mx[v]] ? p : mx[v];
	ans[v] = (f[x] <= n-dep-sz[x])&&(!((n-dep)&1));
	for(int i = head[v];i;i = e[i].nxt){
		if(e[i].to == fa) continue;
		dfs2(e[i].to,v,dep+1,e[i].to == mx[v] ? (sz[p] > sz[cmx[v]] ? p : cmx[v]) : (sz[p] > sz[mx[v]] ? p : mx[v]));
	}
}

inline void Solve(){
	scanf("%d",&n);cnt = 0;FOR(i,1,n) head[i] = 0;
	FOR(i,2,n){
		int u,v;scanf("%d%d",&u,&v);add(u,v);
	}
	dfs1(1);
	dfs2(1);
	FOR(i,1,n){
		putchar('0'+ans[i]);
		if(W == 3) break;
	}
	puts("");
}

int main(){
	int T;scanf("%d%d",&W,&T);
	while(T--) Solve();
	return 0;
}
posted @ 2020-10-14 21:51  RainAir  阅读(81)  评论(0编辑  收藏  举报