正睿2018暑假集训 比赛题选做

Posted dysyn1314

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正睿2018暑假集训 比赛题选做相关的知识,希望对你有一定的参考价值。

题解 ZR246 数对子

题目大意

我们定义一个数对((x,y))是好的,当且仅当(x≤y),且(xoperatorname{xor} y)的二进制表示下有奇数个(1)

现在给定(n)个区间([l_i,r_i]),你需要对于每个(iin[1,n]),输出有几对好的数((x,y))满足(x)(y)都在 ([l_1,r_1]cup[l_2,r_2]dotscup[l_i,r_i]),即两个数都在前(i)个区间的并里。

数据范围:(1leq nleq 10^5), (1leq l_ileq r_ileq 2^{32}-1)

本题题解

异或有一个特性:两个数异或,带来的(二进制下)(1)的数量的变化,永远是偶数个:要么不变、要么减少(2)

由此可知:(xoperatorname{XOR} y)有奇数个(1),当且仅当(x), (y)中一个是奇数,一个是偶数。

设所有数中(1)的数量为偶数的数有( ext{cnt0})个,(1)的数量为奇数的数有( ext{cnt1})个。则答案就是( ext{cnt0} imes ext{cnt1})

我们要支持插入线段、维护( ext{cnt0})( ext{cnt1})。可以用线段树实现。

建一个动态开点线段树,值域为([0,2^{32}-1])。则线段树上每个节点,都表示了一段形如([a2^b,(a+1)2^b-1])的区间,这样的区间(长度等于(1)的除外)里,( ext{cnt0})( ext{cnt1})永远是相等的:都等于区间长度的一半。这就很方便我们用线段树维护我们需要的信息。

时间复杂度(O(nlog n))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

const int MAXN=1e5;
const ll MAXM=(1LL<<32)-1;

int bitcnt(ll x){
	int res=0;
	while(x){
		res+=(x&1LL);
		x>>=1;
	}
	return res;
}
pll operator+(pll x,pll y){
	return mk(x.fi+y.fi,x.se+y.se);
}
void operator+=(pll& x,pll y){
	x=x+y;
}

int rt;
struct SegmentTree{
	static const int SIZE=MAXN*80;
	int ls[SIZE+5],rs[SIZE+5],cov[SIZE+5],tot;
	ll cnt0[SIZE+5],cnt1[SIZE+5];
	void upd(int p,ll l,ll r){
		if(cov[p]){
			if(l==r){
				cnt0[p]=cnt1[p]=0;
				if(bitcnt(l)&1)cnt1[p]=1;
				else cnt0[p]=1;
			}
			else{
				cnt0[p]=cnt1[p]=(r-l+1)/2;
			}
		}
		else{
			if(l==r){
				cnt0[p]=cnt1[p]=0;
			}
			else{
				cnt0[p]=cnt0[ls[p]]+cnt0[rs[p]];
				cnt1[p]=cnt1[ls[p]]+cnt1[rs[p]];
			}
		}
	}
	void modify(int& p,ll l,ll r,ll ql,ll qr,int x){
		if(!p)p=++tot;
		if(ql<=l&&qr>=r){
			cov[p]+=x;
			upd(p,l,r);
			return;
		}
		ll mid=(l+r)>>1;
		if(ql<=mid)modify(ls[p],l,mid,ql,qr,x);
		if(qr>mid)modify(rs[p],mid+1,r,ql,qr,x);
		upd(p,l,r);
	}
	SegmentTree(){}
}T;

int main() {
	int n;cin>>n;while(n--){
		ll l,r;cin>>l>>r;
		T.modify(rt,0,MAXM,l,r,1);
		cout<<T.cnt0[rt]*T.cnt1[rt]<<endl;
	}
	return 0;
}

题解 ZR247 逆序对

题目大意

给定一个偶数(N),现在蔡老板得到了一个由([1,N])内的所有偶数构成的排列(b[1..N/2])

现在蔡老板又得到了一个数组(a[1..N/2]),其中(a[i]=icdot 2?1)

蔡老板想知道,对于所有满足(a)(b)都是它的子序列的([1,N])的排列(p)(p)的逆序对数的最小值。

数据范围:(1leq Nleq 2 imes 10^5)

本题题解

逆序对数=偶数序列自身的逆序对数+奇数序列中每个数插入在偶数序列某个位置时产生的贡献。具体来说,插入在偶数序列某个位置时,产生的贡献就是偶数序列中该位置前面大于该奇数的数的数量+后面小于该奇数的数的数量。

考虑从小到大依次把每个奇数插入。每次都从对于该奇数最优的位置插入。这样的逆序对数一定是最少的。但是会不会出现:较小的奇数插在了较后面的位置,较大的奇数插在了较前面的位置,从而导致不满足“奇数序列是结果序列的一个子序列”这一要求呢?并不会。因为如果每次都选择最优的位置,会发现最优的插入位置是单调递增(可能非严格,但是没关系)的。

具体来说,共有(frac{n}{2}+1)个可能的插入位置。我们用线段树维护每个插入位置的代价。从小到大考虑每个奇数。插入完一个奇数后,变成下一个奇数时,需要对线段树做的修改是:让一段后缀(-1),让一段前缀(+1):这样显然最优的位置单调递增。

时间复杂度(O(nlog n))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=2e5;
int n,a[MAXN+5],pos[MAXN+5];
struct SegmentTree{
	int mn[MAXN*4+5],tag[MAXN*4+5];
	void push_up(int p){
		mn[p]=min(mn[p<<1],mn[p<<1|1]);
	}
	void build(int p,int l,int r){
		if(l==r){
			mn[p]=l-1;
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void upd(int p,int x){
		mn[p]+=x;
		tag[p]+=x;
	}
	void push_down(int p){
		if(tag[p]!=0){
			upd(p<<1,tag[p]);
			upd(p<<1|1,tag[p]);
			tag[p]=0;
		}
	}
	void range_add(int p,int l,int r,int ql,int qr,int x){
		if(ql<=l&&qr>=r){
			upd(p,x);return;
		}
		push_down(p);
		int mid=(l+r)>>1;
		if(ql<=mid)range_add(p<<1,l,mid,ql,qr,x);
		if(qr>mid)range_add(p<<1|1,mid+1,r,ql,qr,x);
		push_up(p);
	}
	SegmentTree(){}
}T;
struct FenwickTree{
	//单点加,后缀求和
	int c[MAXN+5];
	void modify(int p,int x){
		for(;p;p-=(p&(-p)))c[p]+=x;
	}
	int query(int p){
		int res=0;
		for(;p<=n*2;p+=(p&(-p)))res+=c[p];
		return res;
	}
	FenwickTree(){}
}F;
int main() {
	cin>>n;n/=2;
	ll ans=0;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		pos[a[i]]=i;
		ans+=F.query(a[i]);
		F.modify(a[i],1);
	}
	T.build(1,1,n+1);
	for(int i=1;i<=n;++i){
		ans+=T.mn[1];
		T.range_add(1,1,n+1,pos[i*2]+1,n+1,-1);
		T.range_add(1,1,n+1,1,pos[i*2],1);
	}
	cout<<ans<<endl;
	return 0;
}

题解 ZR250 18AB-day2 配对

题目大意

给定一棵(n)个点的边有长度的无根树,小 A 的班里一共有(m)个男生和(m)个女生,他们各自会等概率出现在树上(n)个点中的某一个,(注意同一个点上可能会出现多个人)。

然后小 A 会将他们配对成(m)对男女,设(dis_i)是第(i)对男女在树上的最短距离,小 A 会选择使得(sum_{i=1}^{m}dis_i)最大的配对方案。

现在小 A 想知道,他选择的配对方案中(sum_{i=1}^{m}dis_i)的期望,由于答案可能过大,你只需要输出答案(*n^{2m})后对(10^9+7)取模的值,这个值显然是一个整数。

数据范围:(n,mleq 2500)

本题题解

考虑每条边对答案的贡献。设这条边两侧的节点数分别为(x,y) ((x+y=n)),边权为(w)。考虑它对答案的贡献,我们可以枚举在(x)里有多少男生,多少女生,则贡献为:

[sum_{i=0}^{m}sum_{j=0}^{m}{mchoose i}{mchoose j}x^ix^jy^{m-i}y^{m-j}cdot(min(i,m-j)+min(j,m-i))cdot w ]

如果对所有边暴力计算,总时间复杂度(O(nm^2))。特判(w=0)的情况,可得(70)分。

考虑用前缀和来优化这个式子。具体来说,对每条边,我们先预处理出(f,f_1,f_2)三个数组:

[f[j]={mchoose j}x^{j}y^{m-j}f_1[j]=f[j]cdot jf_2[j]=f[j]cdot(m-j) ]

对这三个数组做前缀和。然后枚举(i),通过分类讨论,把(min)拆开。用预处理好的(f,f_1,f_2)数组,就可以(O(1))计算所有(j)的情况。

时间复杂度(O(nm))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
const int MAXN=2500;
int fac[MAXN+5],ifac[MAXN+5];
inline int comb(int n,int k){
	if(n<k)return 0;
	return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim=MAXN){
	fac[0]=1;
	for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	ifac[lim]=pow_mod(fac[lim],MOD-2);
	for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}

int n,m;
vector<pii>G[MAXN+5];

int sz[MAXN+5],ans,pw[MAXN+5][MAXN+5],f[MAXN+5],f1[MAXN+5],f2[MAXN+5];
void dfs(int u,int fa){
	sz[u]=1;
	for(int i=0;i<SZ(G[u]);++i){
		int v=G[u][i].fi;
		if(v==fa)continue;
		dfs(v,u);
		if(G[u][i].se){
			for(int y=0;y<=m;++y){
				f[y]=(ll)comb(m,y)*pw[sz[v]][y]%MOD*pw[n-sz[v]][m-y]%MOD;
				f1[y]=(ll)f[y]*y%MOD;
				f2[y]=(ll)f[y]*(m-y)%MOD;
				if(y)add(f[y],f[y-1]),add(f1[y],f1[y-1]),add(f2[y],f2[y-1]);
			}
			for(int x=0;x<=m;++x){
				int fx=(ll)comb(m,x)*pw[sz[v]][x]%MOD*pw[n-sz[v]][m-x]%MOD;
				add(ans,(ll)G[u][i].se*fx%MOD*x%MOD*f[m-x]%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*mod2(f2[m]-f2[m-x])%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*(m-x)%MOD*mod2(f[m]-f[m-x])%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*f1[m-x]%MOD);
			}
		}
		sz[u]+=sz[v];
	}
}

int main() {
	facinit();
	for(int i=1;i<=MAXN;++i){
		pw[i][0]=1;
		for(int j=1;j<=MAXN;++j){
			pw[i][j]=(ll)pw[i][j-1]*i%MOD;
		}
	}
	cin>>n>>m;
	for(int i=1,u,v,w;i<n;++i)cin>>u>>v>>w,G[u].pb(mk(v,w)),G[v].pb(mk(u,w));
	dfs(1,0);
	cout<<ans<<endl;
	return 0;
}

题解 ZR252 18ABday3-亵渎

题目大意

YJC最近在研究炉石。今天他在研究关于其中三张卡牌的一个问题:

这三张卡牌描述如下:

  • 香蕉:使一个单位+1生命。
  • 月火术:使一个单位-1生命。
  • 亵渎:使所有单位-1生命,若有单位死亡,则释放亵渎

现在,你的手牌中有足够多的香蕉月火术以及一张亵渎。你正面临着你对手的一个巨大的场面,你需要清空这个场面。 这个场面上有(n)个随从,每个随从的生命值由输入给出。由于种种原因,你所在的这局游戏这三张卡牌的费用不一定是原来的费用,他们将在输入中被给定。 现在你想知道,为了清空这个场面,至少需要多少费用。

如果你完全没有看懂上面讲了什么,可以看下面的简要题意: 你有一个可重集合(A)(A)中的元素是正整数,当一个元素变为(0)时从集合中删除。 你可以花费(p)使一个元素(+1),花费(q)使一个元素(?1)。 你有一次机会花费(r)使得集合中所有元素(?1),若有元素因此被删除,则重复这个操作(不需要额外的花费)。 你的目标是最小化将集合变为空集的花费。

数据范围:(nleq 5000)(1leq a[i]leq 10^6)(0leq p,q,rleq 10^6)

本题题解

我们最后使用亵渎。

考虑什么情况下,能用一次亵渎就清空整个场面。发现,要求所有剩余随从的生命值(排好序后)为从(1)开始的连续若干个值(可以有重复),例如:$1,1,1,2,2,3,4,4,dots $。

假设,我们一开始就选出了一个子集,作为使用亵渎前,还活着的这些随从。在使用亵渎前,子集里的随从的生命值,一定是上述的那种序列。考虑子集里每个随从,如何与序列里的元素对应。显然,一定让初始生命值最小的那个随从,对应序列里最小的元素;初始生命值第二小的,对应序列里第二小的,......,以此类推。

因此,我们先把所有随从,按初始生命值排序。然后做一个DP。设(dp[i][j])表示考虑了前(i)个随从,使用亵渎前的序列,当前最大元素为(j)时,的最小花费。转移时就考虑是否把当前随从加入使用亵渎的序列即可。

时间复杂度(O(n^2))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=5000;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,a[MAXN+5],p,q,r;
ll dp[MAXN+5][MAXN+5];

int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i];
	cin>>p>>q>>r;
	sort(a+1,a+n+1);
	memset(dp,0x3f,sizeof(dp));
	dp[0][0]=0;
	for(int i=1;i<=n;++i){
		for(int j=0;j<i;++j)if(dp[i-1][j]!=INF){
			dp[i][j]=min(dp[i][j],dp[i-1][j]+(ll)a[i]*q);
			ll cost=0;
			if(a[i]<j)cost=(ll)p*(j-a[i]);
			else if(a[i]>j)cost=(ll)q*(a[i]-j);
			dp[i][j]=min(dp[i][j],dp[i-1][j]+cost);
			cost=0;
			if(a[i]<j+1)cost=(ll)p*(j+1-a[i]);
			else if(a[i]>j+1)cost=(ll)q*(a[i]-(j+1));
			dp[i][j+1]=min(dp[i][j+1],dp[i-1][j]+cost);
		}
	}
	ll ans=INF;
	for(int j=0;j<=n;++j)ans=min(ans,dp[n][j]+r*(j>0));
	cout<<ans<<endl;
	return 0;
}

题解 ZR255 18ABday4-世界杯

题目大意

YJC最近在研究世界杯,他拿一半财产压了德国队,另一半财产压了阿根廷队,结果可想而知。YJC表示非常angry,于是又开始研究博彩公司的盈利原理。

假设在世界杯决赛前,有(n)个人参与了赌博。第(i)个人认为法国队赢的概率是(p[i]),克罗地亚队赢的概率是(1?p[i])。对于每一只球队,如果根据博彩公司给出的赔率第(i)个人的期望收益非负,则他会给这只球队下注(a[i])元(设赔率为(x),某人下注了(y)元,如果他赢了可以返还(x?y)元,他输了则会返还(0)元。不论输赢,下注的钱均不返还)。如果两只球队期望收益均非负,则他会给两只球队各下注(a[i])元。

现在博彩公司要决定法国队和克罗地亚队的赔率,使得在自身收益最小的胜负情况下的收益最大。

数据范围:(1leq nleq 10^6), (0leq a[i]leq 100), (0leq p[i]leq 1)。输入、输出精度均为(6)位小数。

本题题解

考虑第(i)个人。设(p_1(i))表示他认为法国队获胜的概率,(p_2(i))表示他认为克罗地亚队获胜的概率,即:(p_1(i)=p[i],p_2(i)=1-p[i])。那么,这个人会下注球队(t) ((tin{1,2})),当且仅当(a[i]cdot p_t(i)cdot xgeq a[i]),其中左边是他期望赢得的金额。把这个不等式化简,得:(xgeq frac{1}{p_t(i)})

(f_t(i)=frac{1}{p_t(i)}),那么,第(i)个人会下注球队(t),当且仅当:(xgeq f_t(i))。所以,对每个球队,有效的赔率取值只有(n+1)(即小于所有(f_t(i)),或者刚好等于(n)个中的某个(f_t(i)))。

暴力的做法是枚举两个球队的赔率分别为多少,再计算收益。时间复杂度(O(n^3))

发现,收益对于两个球队的赔率是可以分别计算的。具体来说,对于球队(t),假设赔率为(x),那么:

  • 如果(t)获胜,博彩公司亏损的金额为:(K_t(x)=sum_{i=1}^{n}[xgeq f_t(i)]cdot (x-1)cdot a[i])
  • 如果(t)失败,博彩公司赚到的金额为:(Z_t(x)=sum_{i=1}^{n}[xgeq f_t(i)]cdot a[i])

求出(K_t)(Z_t)后,我们枚举两个球队分别的赔率:(x_i), (x_j),则盈利为:(minig(Z_1(x_i)-K_2(x_j),Z_2(x_j)-K_1(x_i)ig)),分别代表了球队(1)获胜和球队(2)获胜的情况。暴力求(Z,K),再暴力枚举(x_i,x_j),时间复杂度(O(n^2))

注意到,如果把(f)数组排好序,再从大到小枚举所有(x),则(Z,K)都可以用 two pointers (O(n))求出。

现在,瓶颈在于枚举(x_i,x_j)。后面的(min(dots ))这个式子比较丑。我们把问题写成更简洁的形式:

给定四个数组(A_1[1dots n],A_2[1dots n],B_1[1dots n],B_2[1dots n])。求:(max_{i,jleq n}{min(A_1[i]+B_1[j],A_2[i]+B_2[j])})

这里的((A_1,A_2)),可以看做一个( exttt{pair})型的数组(A),因为(A_1,A_2)出现时下标一样。((B_1,B_2))同理。对(A)数组,按(A_1)从小到大排序。对(B)数组,按(B_1)从小到大排序。(也就是( exttt{pair})默认的比较方式)。

考虑二分答案( ext{mid})。我们要判断是否存在一对(i,j),满足(A_1[i]+B_1[j])(A_2[i]+B_2[j])均大于等于( ext{mid})

枚举(A)的下标(i)。那么,能够满足(A_1[i]+B_1[j]geq ext{mid})(j),一定是(B)序列的一段后缀。我们只需要拿这个后缀里,(B_2[j])最大的(j),看(B_2[j]+A_2[i])是否( ext{mid})即可。这段后缀的长度可以在枚举(i)时用 two pointers 维护。后缀最大的(B_2),可以预处理出来。

时间复杂度(O(nlog n+nlog( ext{eps}^{-1})))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef double db;

const int MAXN=1e6;
const db INF=1e18;
const db EPS=1e-8;
int n,cntx1,cntx2;
struct People_t{
	db a,p1,p2,needx1,needx2;
}a[MAXN+5];
bool cmp_needx1(People_t a,People_t b){
	return a.needx1<b.needx1;
}
bool cmp_needx2(People_t a,People_t b){
	return a.needx2<b.needx2;
}
db x1[MAXN+5],x2[MAXN+5];
pair<db,db>A[MAXN+5],B[MAXN+5];

int main() {
	//freopen("ex_a2.in","r",stdin);
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>a[i].a>>a[i].p1;
		a[i].p2=(db)1-a[i].p1;
		if(a[i].p1!=0)a[i].needx1=(db)1/a[i].p1,x1[++cntx1]=a[i].needx1;else a[i].needx1=INF;
		if(a[i].p2!=0)a[i].needx2=(db)1/a[i].p2,x2[++cntx2]=a[i].needx2;else a[i].needx2=INF;
	}
	x1[++cntx1]=0;x2[++cntx2]=0;
	
	sort(a+1,a+n+1,cmp_needx1);
	sort(x1+1,x1+cntx1+1);
	for(int i=1,j=0;i<=cntx1;++i){
		A[i].fi=-A[i-1].se*(x1[i]-1);
		A[i].se=A[i-1].se;
		while(j+1<=n && a[j+1].needx1<=x1[i]){
			++j;
			A[i].fi-=(x1[i]-1)*a[j].a;
			A[i].se+=a[j].a;
		}
	}
	
	sort(a+1,a+n+1,cmp_needx2);
	sort(x2+1,x2+cntx2+1);
	for(int i=1,j=0;i<=cntx2;++i){
		B[i].fi=B[i-1].fi;
		B[i].se=-B[i-1].fi*(x2[i]-1);
		while(j+1<=n && a[j+1].needx2<=x2[i]){
			++j;
			B[i].se-=(x2[i]-1)*a[j].a;
			B[i].fi+=a[j].a;
		}
	}
	
	sort(A+1,A+cntx1+1);
	sort(B+1,B+cntx2+1);
	B[cntx2+1].se=-INF;
	for(int i=cntx2;i>=1;--i){
		if(B[i+1].se>B[i].se)B[i].se=B[i+1].se;//后缀max
	}
	double l=0,r=INF;
	while(r-l>EPS){
		double mid=(l+r)/((db)2);
		bool ok=false;
		for(int i=1,j=cntx2+1;i<=cntx1;++i){
			while(j>1 && A[i].fi+B[j-1].fi>=mid)--j;
			if(A[i].se+B[j].se>=mid){
				ok=true;break;
			}
		}
		if(ok)l=mid;
		else r=mid;
	}
	cout<<setiosflags(ios::fixed)<<setprecision(6)<<l<<endl;
	return 0;
}

题解 ZR256 18ABday4-数组

题目大意

YJC最近在研究数组,他认为如果数组的一个区间内不包含重复的元素,那么这个区间是一个优美的区间。

现在YJC弄到了一个interesting的数组,他想知道这个数组有多少个优美的区间。当然,他有时候也会觉得这个数组不够interesting,此时他会修改数组中的一个元素。

YJC发现在修改之后他不会算有多少个优美的区间了,于是他来向你求助。

数据范围:(1leq nleq 10^5), (qleq 2n), (1leq a_ileq n)。((a_i)是数组中的元素)

本题题解

( ext{pre}[i])表示位置(i)上的数,在序列里上一次出现的位置;记( ext{nxt}[i])表示位置(i)上的数,在序列里下一次出现的位置。像本题这种限制数在区间里出现次数的问题,往往可以转化为( ext{pre}[i])( ext{nxt}[i])之间的关系。这是一个小套路。

考虑枚举区间的右端点(i),看最靠前的、合法的左端点在哪里,记这个位置为(f_i)。那么,左端点在([f_i,i])之间都是合法的区间。故答案为(sum_{i=1}^{n}(i-f_i+1))。容易发现,以(i)为右端点的(f_i),一定至少大于等于(f_{i-1})。同时,为了保证(i)这个数在区间里只出现一次,(f_i)又需要大于( ext{pre}[i])。所以,(f_{i}=max(f_{i-1}, ext{pre}[i]+1))。有了这个递推式,再看求答案的式子,发现答案就等于(sum_{i=1}^{n}(i-max_{j=1}^{j} ext{pre}[j])),于是问题转化为维护( ext{pre})数组的“前缀(max)”之和。

一次修改操作,只会对不超过(3)个点的( ext{pre})值产生影响,分别是:(x),原来的( ext{nxt}[x]),新的( ext{nxt}[x])这三个位置。这几个位置是很容易求出的。我们可以开(n)( exttt{set}),记录每个值在哪些位置出现过。然后在( exttt{set})里二分,就能找到这几个要修改的位置。

于是问题转化为,维护一个序列,支持单点修改,询问“前缀最大值”之和

考虑用线段树维护。对线段树上,每个节点(p=[l,r]),我们记录两个值:

  • ( ext{mx}[p]),表示区间([l,r])( ext{pre})数组的最大值。
  • ( ext{sum}[p]),表示区间([l,r])里,每个位置(i),从(l)(i)的“前缀最大值”之和。

难点在于如何( ext{pushup}),也就是合并左、右两个儿子。合并时,( ext{mx})直接取两个儿子的较大值即可。左儿子的( ext{sum}),可以直接贡献到当前节点上。而右儿子的里,需要先对左儿子的最大值( ext{mx}[ ext{left_son}])(max),再累加到( ext{sum}[p])中。怎么求线段树里某个区间对某个值取(max)后的和呢?我们可以递归下去,在(O(log n))的时间里求出。具体代码如下:

ll cmax_sum(int p,int l,int r,int x){
	if(l==r)return max(x,mx[p]);
	int mid=(l+r)>>1;
	if(mx[p<<1]<=x)return (ll)x*(mid-l+1)+cmax_sum(p<<1|1,mid+1,r,x);
	else return cmax_sum(p<<1,l,mid,x)+sum[p]-sum[p<<1];
}
void push_up(int p){
	int ls=p<<1,rs=p<<1|1;
	mx[p]=max(mx[ls],mx[rs]);
	sum[p]=sum[ls]+cmax_sum(rs,tl[rs],tr[rs],mx[ls]);
}

时间复杂度(O(nlog^2n))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=1e5;
int n,a[MAXN+5],pos[MAXN+5],pre[MAXN+5],nxt[MAXN+5];
set<int>s[MAXN+5];

struct SegmentTree{
	ll sum[MAXN*4+5];
	int mx[MAXN*4+5];
	int tl[MAXN*4+5],tr[MAXN*4+5];
	ll cmax_sum(int p,int l,int r,int x){
		if(l==r)return max(x,mx[p]);
		int mid=(l+r)>>1;
		if(mx[p<<1]<=x)return (ll)x*(mid-l+1)+cmax_sum(p<<1|1,mid+1,r,x);
		else return cmax_sum(p<<1,l,mid,x)+sum[p]-sum[p<<1];
	}
	void push_up(int p){
		int ls=p<<1,rs=p<<1|1;
		mx[p]=max(mx[ls],mx[rs]);
		sum[p]=sum[ls]+cmax_sum(rs,tl[rs],tr[rs],mx[ls]);
	}
	void build(int p,int l,int r){
		tl[p]=l;tr[p]=r;
		if(l==r){
			sum[p]=mx[p]=pre[l];
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void modify(int p,int l,int r,int pos,int x){
		if(l==r){
			sum[p]=mx[p]=x;
			return;
		}
		int mid=(l+r)>>1;
		if(pos<=mid)modify(p<<1,l,mid,pos,x);
		else modify(p<<1|1,mid+1,r,pos,x);
		push_up(p);
	}
	SegmentTree(){}
}T;

int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i],s[a[i]].insert(i);
	for(int i=1;i<=n;++i)pre[i]=pos[a[i]],pos[a[i]]=i;
	for(int i=1;i<=n;++i)pos[i]=n+1;
	for(int i=n;i>=1;--i)nxt[i]=pos[a[i]],pos[a[i]]=i;
	T.build(1,1,n);
	ll sum=(ll)n*(n+1)/2;
	int q;cin>>q;while(q--){
		int op;cin>>op;
		if(!op){
			cout<<sum-T.sum[1]<<endl;
		}
		else{
			int p,v;cin>>p>>v;
			if(v==a[p])continue;
			if(nxt[p]!=n+1){
				pre[nxt[p]]=pre[p];
				T.modify(1,1,n,nxt[p],pre[nxt[p]]);
			}
			if(pre[p]!=0)nxt[pre[p]]=nxt[p];
			s[a[p]].erase(p);
			set<int>::iterator it=s[v].lob(p);
			if(it!=s[v].end()){
				nxt[p]=*it;
				pre[nxt[p]]=p;
				T.modify(1,1,n,nxt[p],pre[nxt[p]]);
			}
			else nxt[p]=n+1;
			if(it!=s[v].begin()){
				--it;
				pre[p]=*it;
				nxt[pre[p]]=p;
			}
			else pre[p]=0;
			T.modify(1,1,n,p,pre[p]);
			s[v].insert(p);
			a[p]=v;
			//cout<<"pre ";for(int i=1;i<=n;++i)cout<<pre[i]<<" ";cout<<endl;
		}
	}
	return 0;
}

题解 ZR258 18ABday5-友谊巨轮

题面下载

本题题解

先不管问题。我们尝试维护出每个时刻每个人的巨轮是谁。直观的做法是用一个二维数组,存pair<long long,int>类型,分别表示两人之间的消息数量和最近一次发消息的时间。我们需要支持单点修改,并维护这个数组每一行的最大值。直观的做法是每行开一个线段树,但这样空间会超限。改成动态开点线段树就好了:因为修改次数是(O(n))的,每次修改只会在线段树上新增(O(log n))个节点,所以空间复杂度是(O(nlog n)),可以接受。

维护出每个人的巨轮后,考虑回答询问。发现每个时刻,只有不超过(4)个人的巨轮会变化(新加入的一对人,和刚刚过期的一对人)。我们说一个人的“状态”是指,他是否是单向巨轮。那么,如果(x)这个人的巨轮变化了,那么我们只需要更新:(1) 原来(x)的巨轮;(2) (x);(3) (x)的新巨轮;这三个点的状态。容易发现,其他点的状态不会改变。所以可以(O(1))完成所有更新。

时间复杂度(O(nlog n))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=1e5;
int n,t,m;
map<pii,pair<ll,int> >mp;
struct Event_t{
	int a,b,c;
	Event_t(){}
	Event_t(int _a,int _b,int _c){
		a=_a;
		b=_b;
		c=_c;
	}
}q[MAXN+5];

const int SIZE=2e7;
int rt[MAXN+5],cnt,mxpos[SIZE],ls[SIZE],rs[SIZE];
pair<ll,int> mx[SIZE];
int newnode(){
	++cnt;
	ls[cnt]=rs[cnt]=mxpos[cnt]=0;
	mx[cnt]=mk(0,0);
	return cnt;
}
void push_up(int p){
	if(!ls[p] && !rs[p])return;
	if(!ls[p]){
		mx[p]=mx[rs[p]];
		mxpos[p]=mxpos[rs[p]];
		return;
	}
	if(!rs[p]){
		mx[p]=mx[ls[p]];
		mxpos[p]=mxpos[ls[p]];
		return;
	}
	if(mx[ls[p]]>mx[rs[p]]){
		mx[p]=mx[ls[p]];
		mxpos[p]=mxpos[ls[p]];
	}
	else{
		mx[p]=mx[rs[p]];
		mxpos[p]=mxpos[rs[p]];
	}
}
void modify(int& p,int l,int r,int pos,const pair<ll,int>& x){
	int tmp=p;
	p=newnode();
	if(tmp){
		ls[p]=ls[tmp];
		rs[p]=rs[tmp];
	}
	if(l==r){
		mx[p]=x;
		mxpos[p]=l;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)
		modify(ls[p],l,mid,pos,x);
	else
		modify(rs[p],mid+1,r,pos,x);
	push_up(p);
}
int ans;
bool in_ans[MAXN+5];
void update_ans(int a){
	if(in_ans[a])ans--;
	in_ans[a]=0;
	if(rt[a] && mx[rt[a]].fi!=0){
		int f=mxpos[rt[a]];
		if(mxpos[rt[f]]!=a){
			in_ans[a]=1;
			ans++;
		}
	}
}
vector<int>v;
void tree_modify(int a,int b){
	if(rt[a] && mx[rt[a]].fi!=0){
		v.pb(mxpos[rt[a]]);
	}
	if(rt[b] && mx[rt[b]].fi!=0){
		v.pb(mxpos[rt[b]]);
	}
	modify(rt[a],1,n,b,mp[mk(a,b)]);
	modify(rt[b],1,n,a,mp[mk(a,b)]);
	if(rt[a] && mx[rt[a]].fi!=0){
		v.pb(mxpos[rt[a]]);
	}
	if(rt[b] && mx[rt[b]].fi!=0){
		v.pb(mxpos[rt[b]]);
	}
}
void solve(){
	cnt=0;ans=0;
	mp.clear();
	for(int i=1;i<=n;++i){
		rt[i]=0;
		in_ans[i]=0;
	}
	for(int i=1;i<=t;++i){
		cin>>q[i].a>>q[i].b>>q[i].c;
		if(q[i].a>q[i].b)swap(q[i].a,q[i].b);
		
		mp[mk(q[i].a,q[i].b)].fi+=q[i].c;
		mp[mk(q[i].a,q[i].b)].se=i;
		tree_modify(q[i].a,q[i].b);
		
		if(i-m>=1){
			mp[mk(q[i-m].a,q[i-m].b)].fi-=q[i-m].c;
			if(mp[mk(q[i-m].a,q[i-m].b)].se==i-m){
				mp[mk(q[i-m].a,q[i-m].b)].se=0;
			}
			tree_modify(q[i-m].a,q[i-m].b);
			update_ans(q[i-m].a);
			update_ans(q[i-m].b);
		}
		//for(int j=1;j<=n;++j)update_ans(j);
		for(int i=0;i<SZ(v);++i){
			update_ans(v[i]);
		}
		v.clear();
		update_ans(q[i].a);
		update_ans(q[i].b);
		cout<<ans<<endl;
	}
}
int main() {
	int Tes;cin>>Tes;while(Tes--){
		cin>>n>>t>>m;
		solve();
	}
	return 0;
}

题解 ZR260 18ABday5-构解巨树

题面下载

本题题解

首先,给定一棵树,如何求出所有点对距离和?朴素做法是枚举两个点,求一遍距离,时间复杂度(O(n^3))。如果求距离用(O(1))LCA的方法求,则优化到(O(n^2))。换个角度,累加“每条边的贡献”,例如,考虑(u)(fa(u))之间的边,则答案为(sum_{u eq 1} ext{size}_ucdot (n- ext{size}_u)),于是只需要求出每个点的( ext{size})即可,时间复杂度(O(n))。本题就是在这种考虑“每条边贡献”的思想的基础上完成的。

回到本题,也就是这个“树套树”的模型中。我们把贡献分为三类:

  1. 小树和小树之间的边。也就是(m)条边之一。这些边两侧各有若干棵完整的小树。设它子树里有(x)棵小树,则对答案的贡献就是(xcdot ncdot (mcdot n-xcdot n))。于是直接对大树dfs,就可以(O(m))求出这类边的贡献。
  2. 小树内部的边,同一小树内点对经过这条边的次数。这样的贡献,对每棵小树的同一条边来说是相同的。所以只需要对小树做一遍dfs,求出贡献和后乘以(m)就行。时间复杂度(O(n))
  3. 小树内部的边,来自不同小树点对经过这条边的次数。

第3类贡献比较麻烦。我们来仔细讨论。

依次考虑每棵小树。对当前小树,先以(1)为根,把它看成一棵有根树。然后转化为,有一棵树,然后给定一些二元组((x,y)),表示在节点(x)上,挂了(ycdot n)个点。

我们记小树节点(u)子树内的点数为( ext{in}(u)),子树外的点数为( ext{out}(u))。初始时,只考虑了当前小树里的点(没有考虑二元组挂上去的点),也就是( ext{in}(u)= ext{size}_u, ext{out}(u)=n- ext{size}_u)。请注意,这里(u eq 1),因为(1)节点和父亲之间没有边,自然也不会产生贡献。然后我们依次考虑每个二元组((x,y)),把它挂上去时,会新增的贡献数是:

  • 对于(x)的所有祖先(u)(不含(1)),答案增加(ycdot ncdot ext{out}(u))
  • 对于其他节点(v)(不含(1)),答案增加(ycdot ncdot ext{in}(v))

然后用二元组((x,y))来更新每个节点的( ext{in}, ext{out})

  • 对于(x)的所有祖先(u)(不含(1)),( ext{in}(u))增加(ycdot n)
  • 对于其他节点(v)(不含(1)),( ext{out}(v))增加(ycdot n)

可以看出,我们要支持,(1) 给所有祖先加,(2) 给所有其他节点加。而“给其他节点加”,可以转化为先给所有节点加,然后再给祖先减掉。所以只需要支持对所有祖先加,对所有祖先求和即可。可以用树链剖分+线段树实现。

虽然每个小树上可能挂多个二元组,但二元组的本质就是大树上的边,所以总数只有(O(m))个。每次树链剖分+线段树操作的复杂度是(O(log^2n))的,所以总时间复杂度(O(mlog^2n))

有一个小问题是,每考虑完一个小树后,要清空线段树。准确来说不是清空,是还原成( ext{in}(u)= ext{size}_u, ext{out}(u)=n- ext{size}_u)的情况。一种做法是把所有操作反着做一遍:即区间加变成区间减。但是这样常数一下子翻倍。有一种更巧妙的方法是维护时间戳。在线段树上走到一个节点时,如果它的时间戳和当前时间不一样,说明它还停留在之前的某个阶段,我们直接把它还原,然后把它的时间戳改成当前时间即可。

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MOD=1e9+7;
const int MAXN=1e5;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}

int n,m,ans;

namespace SmallTree{
struct EDGE{int nxt,to;}edge[MAXN*2+5];
int head[MAXN+5],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;}

int fa[MAXN+5],sz[MAXN+5],son[MAXN+5],dep[MAXN+5];
void dfs1(int u){
	sz[u]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u])continue;
		fa[v]=u;
		dep[v]=dep[u]+1;
		dfs1(v);
		sz[u]+=sz[v];
		if(!son[u]||sz[v]>sz[son[u]])son[u]=v;
	}
}
int top[MAXN+5],dfn[MAXN+5],ofn[MAXN+5],rev[MAXN+5],cnt_dfn;
void dfs2(int u,int t){
	top[u]=t;
	dfn[u]=++cnt_dfn;
	rev[cnt_dfn]=u;
	if(son[u])dfs2(son[u],t);
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
	}
	ofn[u]=cnt_dfn;
}
}//namespace SmallTree

struct SegmentTree{
	int curtim;
	int sum[MAXN*4+5],tag[MAXN*4+5],initsum[MAXN*4+5],tim[MAXN*4+5];
	void push_up(int p){
		sum[p]=mod1(sum[p<<1]+sum[p<<1|1]);
	}
	void build(int p,int l,int r,bool flag){
		// flag:
		// in 0; out 1
		if(l==r){
			using SmallTree::sz;
			using SmallTree::rev;
			if(!flag)
				initsum[p]=sum[p]=sz[rev[l]];
			else
				initsum[p]=sum[p]=n-sz[rev[l]];
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid,flag);
		build(p<<1|1,mid+1,r,flag);
		push_up(p);
		initsum[p]=mod1(initsum[p<<1]+initsum[p<<1|1]);
	}
	void clr(int p){
		if(tim[p]!=curtim){
			tim[p]=curtim;
			sum[p]=initsum[p];
			tag[p]=0;
		}
	}
	void upd(int p,int l,int r,int x){
		add(sum[p],(ll)x*(r-l+1)%MOD);
		add(tag[p],x);
	}
	void push_down(int p,int l,int mid,int r){
		clr(p<<1);
		clr(p<<1|1);
		if(tag[p]){
			upd(p<<1,l,mid,tag[p]);
			upd(p<<1|1,mid+1,r,tag[p]);
			tag[p]=0;
		}
	}
	void range_add(int p,int l,int r,int ql,int qr,int x){
		clr(p);
		if(ql<=l && qr>=r){
			upd(p,l,r,x);
			return;
		}
		int mid=(l+r)>>1;
		push_down(p,l,mid,r);
		if(ql<=mid)
			range_add(p<<1,l,mid,ql,qr,x);
		if(qr>mid)
			range_add(p<<1|1,mid+1,r,ql,qr,x);
		push_up(p);
	}
	int query(int p,int l,int r,int ql,int qr){
		clr(p);
		if(ql<=l && qr>=r){
			return sum[p];
		}
		int mid=(l+r)>>1;
		push_down(p,l,mid,r);
		int res=0;
		if(ql<=mid)
			res=query(p<<1,l,mid,ql,qr);
		if(qr>mid)
			add(res,query(p<<1|1,mid+1,r,ql,qr));
		push_up(p);
		return res;
	}
}T_in,T_out;

int jump(int _u,int x){
	using namespace SmallTree;
	int u=_u;
	int res=T_in.query(1,1,n,2,n);
	while(top[u]!=1){
		add(res,T_out.query(1,1,n,dfn[top[u]],dfn[u]));
		sub(res,T_in.query(1,1,n,dfn[top[u]],dfn[u]));
		u=fa[top[u]];
	}
	if(u!=1){
		add(res,T_out.query(1,1,n,dfn[1]+1,dfn[u]));
		sub(res,T_in.query(1,1,n,dfn[1]+1,dfn[u]));
	}
	res=(ll)res*x%MOD;
	
	u=_u;
	T_out.range_add(1,1,n,2,n,x);
	while(top[u]!=1){
		T_in.range_add(1,1,n,dfn[top[u]],dfn[u],x);
		T_out.range_add(1,1,n,dfn[top[u]],dfn[u],mod2(-x));
		u=fa[top[u]];
	}
	if(u!=1){
		T_in.range_add(1,1,n,dfn[1]+1,dfn[u],x);
		T_out.range_add(1,1,n,dfn[1]+1,dfn[u],mod2(-x));
	}
	
	return res;
}

namespace BigTree{
struct EDGE{int nxt,to,from_node,to_node;}edge[MAXN*2+5];
int head[MAXN+5],tot;
inline void add_edge(int u,int v,int a,int b){
	edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;
	edge[tot].from_node=a;
	edge[tot].to_node=b;
}

int sz[MAXN+5];
void dfs_solve(int u,int fa,int connect_node){
	vector<pii>vec;
	sz[u]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs_solve(v,u,edge[i].to_node);
		vec.pb(mk(edge[i].from_node,sz[v]));
		add(ans,(ll)sz[v]*n%MOD*mod2((ll)m*n%MOD-(ll)sz[v]*n%MOD)%MOD);
		sz[u]+=sz[v];
	}
	if(fa!=0){
		vec.pb(mk(connect_node,m-sz[u]));
	}
	T_in.curtim=u;
	T_out.curtim=u;//线段树清空
	for(int i=0;i<SZ(vec);++i){
		add(ans,jump(vec[i].fi,(ll)vec[i].se*n%MOD));
	}
}
}//namespace BigTree

int main() {
	cin>>n>>m;
	for(int i=1;i<=n-1;++i){
		int u,v;cin>>u>>v;
		SmallTree::add_edge(u,v);
	}
	for(int i=1;i<=m-1;++i){
		int u,v,a,b;cin>>u>>v>>a>>b;
		BigTree::add_edge(u,v,a,b);
	}
	SmallTree::dfs1(1);
	SmallTree::dfs2(1,1);
	for(int i=2;i<=n;++i){
		using SmallTree::sz;
		add(ans,(ll)sz[i]*(n-sz[i])%MOD*m%MOD);
	}
	
	T_in.build(1,1,n,0);
	T_out.build(1,1,n,1);
	BigTree::dfs_solve(1,0,0);
	
	cout<<ans<<endl;
	return 0;
}

题解 ZR261 18ABday6-萌新拆塔

题面下载

本题题解

这种类似于全排列的搜索,往往都指向一个优化:状压DP

(dp[mask])表示已经打败了(mask)里这些怪物,剩下的血量的最大值。除血量外,攻击、防御、魔防值都是确定的,可以顺便维护出来。

但是这样会遇到一个问题:什么时候取宝石。我们发现,一打完立即取走宝石并不一定是最优的,因为有“模仿怪”这种东西存在。所以这个状态要改一改。设(dp[s_1][s_2])表示打败了(s_1)里面这些怪物,取走了(s_2)里这些宝石,剩余血量的最大值。显然,(s_2)一定是(s_1)的子集,所以总状态数是(3^n)。在具体实现时,我们不开二维数组,而是用三进制状态来表示:未被打败、被打败了但没有取走宝石、取走了宝石,这三种状态。

时间复杂度(O(3^ncdot n))

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=14;
const int MAX_MASK=5e6;

int pw3[MAXN+5];
void init(){
	pw3[0]=1;
	for(int i=1;i<=MAXN;++i)pw3[i]=pw3[i-1]*3;
}
struct Ternary_t{
	int x;
	Ternary_t(int _x=0){
		x=_x;
	}
	Ternary_t operator>>(const int& rhs){
		Ternary_t res=*this;
		res.x/=pw3[rhs];
		return res;
	}
	Ternary_t& operator>>=(const int& rhs){
		*this=((*this)>>rhs);
		return *this;
	}
	Ternary_t& operator++(){
		x++;
		return *this;
	}
	int binary(){
		int res=0;
		for(int i=0;i<=13;++i)if(((*this)>>i)%3!=0)res^=(1<<i);
		return res;
	}
	operator int()const{return x;}
};

int n,K;
int h,a,d,m;
struct Monster_t{
	int H,A,D,S,a,d,m,h;
	void input(){
		cin>>H>>A>>D>>S>>a>>d>>m>>h;
	}
}mons[MAXN+5];
vector<int>G[MAXN+5];
bool good_mask[1<<MAXN];

struct State_t{
	ll h,a,d,m;
	State_t(){}
	State_t(ll _h,ll _a,ll _d,ll _m){
		h=_h;a=_a;d=_d;m=_m;
	}
	bool operator<(const State_t& rhs){
		return h<rhs.h;
	}
}dp[MAX_MASK];

const State_t FAIL_STATE=State_t(-1,0,0,0);

State_t fight(const State_t& i,int j){
	bool monster_first=(mons[j].S&1);
	bool ignore_my_def=((mons[j].S>>1)&1);
	bool two_att=((mons[j].S>>2)&1);
	bool same_with_me=((mons[j].S>>3)&1);
	
	ll A=mons[j].A;
	ll D=mons[j].D;
	if(same_with_me){A=i.a;D=i.d;}
	
	int you_to_i=A-(ignore_my_def?0:i.d);
	int i_to_you=i.a-D;
	if(i_to_you<=0)return FAIL_STATE;
	
	ll rounds=mons[j].H/i_to_you+(mons[j].H%i_to_you!=0);
	ll you_att=max(0LL,(rounds-(!monster_first))*(two_att?2:1)*you_to_i-i.m);
	
	if(you_att>=i.h)return FAIL_STATE;
	
	State_t res=i;
	res.h-=you_att;
	return res;
}
State_t pick(const State_t& i,int j){
	State_t res=i;
	res.h+=mons[j].h;
	res.a+=mons[j].a;
	res.d+=mons[j].d;
	res.m+=mons[j].m;
	return res;
}

int main() {
	init();
	int T;cin>>T;while(T--){
		cin>>h>>a>>d>>m;
		cin>>n;
		for(int i=1;i<=n;++i){
			mons[i].input();
			vector<int>().swap(G[i]);
		}
		cin>>K;
		for(int i=1;i<=K;++i){
			int u,v;cin>>u>>v;
			G[v].pb(u);
		}
		for(int i=0;i<(1<<n);++i){
			good_mask[i]=true;
			for(int j=1;j<=n;++j)if((i>>(j-1))&1){
				for(int k=0;k<SZ(G[j]);++k){
					if(!((i>>(G[j][k]-1))&1)){
						good_mask[i]=false;
						break;
					}
				}
				if(!good_mask[i])break;
			}
		}
		dp[0]=State_t(h,a,d,m);
		for(Ternary_t mask=1;mask<=pw3[n]-1;++mask){
			dp[mask]=FAIL_STATE;
		}
		for(Ternary_t mask=0;mask<=pw3[n]-2;++mask){
			if(dp[mask].h==-1)continue;
			int bi=mask.binary();
			for(int j=1;j<=n;++j){
				if((mask>>(j-1))%3==0){
					//新打一个怪
					if(good_mask[bi|(1<<(j-1))]){
						State_t tmp=fight(dp[mask],j);
						if(dp[mask+pw3[j-1]]<tmp){
							dp[mask+pw3[j-1]]=tmp;
						}
					}
				}
				else if((mask>>(j-1))%3==1){
					//捡个宝石
					State_t tmp=pick(dp[mask],j);
					if(dp[mask+pw3[j-1]]<tmp){
						dp[mask+pw3[j-1]]=tmp;
					}
				}
			}
		}
		cout<<dp[pw3[n]-1].h<<endl;
	}
	return 0;
}

题解 ZR263 18ABday6-风花雪月

题面下载

本题题解

(s=sum c_i)

首先,抽卡的总次数是有限的。当总卡数达到(4s+3n)时,无论抽到的是什么卡,都必能兑换所有服装。这是因为,每(4)张同一种类的卡,就能兑换任意一种服装。而为了避免卡被模(4)的余数浪费掉,所以预先安排(3n)张卡,专门用于被余数浪费。如果你不能理解(+3n),可以想一想(c={0,0,1}),抽到的卡为({2,2,0})的情况。

现在,考虑手上抽到的卡,把每种卡被抽到的数量的序列,称为一个“状态”。那么,如果当前手上抽到的卡,已经能够获得(n)件衣服,我们称这个状态为“不好”的。否则,也就是无法获得(n)件衣服时,我们称这样的状态是“好的”。

题目问,第一次达到“不好的”状态,所经过的期望操作次数。就相当于问,经过的“好的”状态的数量的期望。另外,显然,“好的”状态之间,是一个严格的树形结构,不会出现环(因为每个状态比它的父亲多一张卡)。所以,每个状态最多只会被经过(1)次。因此,根据期望的线性性,经过的“好的”状态的数量的期望,就等于经过每个“好的”状态的概率之和(其实本质是概率( imes 1)之和,因为最多只会经过(1)次)。

可以用DP求所有“好的”状态的概率之和。依次考虑每种卡。假设当前考虑了前(i)种卡。我们要知道前(i)种卡,能为后面贡献多少张卡,记为(k)。这里的“贡献”,就是指每四张相同类型的卡换任意一张其他卡。例如,如果前(i)种卡里,某一种卡需要(c)张,而实际抽到了(t)张,则会令贡献值(k)增加(lfloorfrac{t-c}{4} floor)。当然,如果前(i)种卡,自己都没有抽满,需要后面的卡来贡献它们,则(k)为负数。

于是可以设计一个DP状态:设(dp[i][j][k])表示考虑了前(i)种卡,总共选出了(j)张卡,贡献值为(k)的方案数。转移时,枚举第(i)种卡选了(t)张。如果(t<c_i),则新的(k‘=k-(c_i-t));否则(k‘=k+lfloorfrac{t-c_i}{4} floor)

在转移时,根据新增的卡数(t),我们乘上系数(frac{(p_i)^t}{t!})。在最后统计答案时,如果选出的总卡数为(J),我们再乘上(J!)。最后就相当于乘以了(frac{J!}{prod(t!)}),这也就是含重复元素的排列计数。

分析时间复杂度。(ileq n,0leq jleq 4s+3n,-sleq kleq s)。转移时,还要枚举一个和(j)同阶的(t)。总时间复杂度(O(n(4s)^2s))卡卡就过了

参考代码:

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

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=10,MAXM=1600,BASE=401;
const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
int fac[MAXM+5],ifac[MAXM+5];
inline int comb(int n,int k){
	if(n<k)return 0;
	return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim){
	fac[0]=1;
	for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	ifac[lim]=pow_mod(fac[lim],MOD-2);
	for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}

int n,p[MAXN+5],c[MAXN+5],suf_c[MAXN+5];
int dp[MAXN+5][MAXM+MAXN*3+5][BASE*2+5];

int main() {
	facinit(MAXM);
	cin>>n;
	int sum=0;
	for(int i=1;i<=n;++i){
		cin>>p[i];
		add(sum,p[i]);
	}
	int isum=pow_mod(sum,MOD-2);
	for(int i=1;i<=n;++i){
		p[i]=(ll)p[i]*isum%MOD;
	}
	sum=0;
	for(int i=1;i<=n;++i){
		cin>>c[i];
		sum+=c[i];
	}
	for(int i=n;i>=1;--i){
		suf_c[i]=suf_c[i+1]+c[i];
	}
	int m=sum*4+n*3;
	dp[0][0][0+BASE]=1;
	for(int i=1;i<=n;++i){
		static int f[MAXM+5];
		int w=1;
		for(int j=0;j<=m;++j){
			f[j]=(ll)w*ifac[j]%MOD;
			w=(ll)w*p[i]%MOD;
		}
		for(int j=0;j<=m;++j){
			for(int k=-sum,v;k<=suf_c[i];++k)if(v=dp[i-1][j][k+BASE]){
				for(int t=0;t+j<=m;++t){
					int newk=k;
					if(t<c[i])newk-=c[i]-t;
					else newk+=(t-c[i])/4;
					if(newk<-sum)continue;
					if(newk>suf_c[i+1])break;
					add(dp[i][t+j][newk+BASE],(ll)v*f[t]%MOD);
				}
			}
		}
	}
	int ans=0;
	for(int j=0;j<=m;++j){
		int tmp=0;
		for(int k=-sum;k<0;++k){
			add(tmp,dp[n][j][k+BASE]);
		}
		add(ans,(ll)tmp*fac[j]%MOD);
	}
	cout<<ans<<endl;
	return 0;
}

以上是关于正睿2018暑假集训 比赛题选做的主要内容,如果未能解决你的问题,请参考以下文章

10.2 正睿国庆集训测试1

图论杂题选做

正睿OI DAY3 杂题选讲

[补档]暑假集训D2总结

CF简单题选做

有趣计数题选做