标签:枚举 event 还原 就是 二分答案 它的 相同 不等式 个人
我们定义一个数对\((x,y)\)是好的,当且仅当\(x≤y\),且\(x\operatorname{xor} y\)的二进制表示下有奇数个\(1\)。
现在给定\(n\)个区间\([l_i,r_i]\),你需要对于每个\(i\in[1,n]\),输出有几对好的数\((x,y)\)满足\(x\)和\(y\)都在 \([l_1,r_1]\cup[l_2,r_2]\dots\cup[l_i,r_i]\),即两个数都在前\(i\)个区间的并里。
数据范围:\(1\leq n\leq 10^5\), \(1\leq l_i\leq r_i\leq 2^{32}-1\)。
异或有一个特性:两个数异或,带来的(二进制下)\(1\)的数量的变化,永远是偶数个:要么不变、要么减少\(2\)。
由此可知:\(x\operatorname{XOR} y\)有奇数个\(1\),当且仅当\(x\), \(y\)中一个是奇数,一个是偶数。
设所有数中\(1\)的数量为偶数的数有\(\text{cnt0}\)个,\(1\)的数量为奇数的数有\(\text{cnt1}\)个。则答案就是\(\text{cnt0}\times\text{cnt1}\)。
我们要支持插入线段、维护\(\text{cnt0}\)和\(\text{cnt1}\)。可以用线段树实现。
建一个动态开点线段树,值域为\([0,2^{32}-1]\)。则线段树上每个节点,都表示了一段形如\([a2^b,(a+1)2^b-1]\)的区间,这样的区间(长度等于\(1\)的除外)里,\(\text{cnt0}\)和\(\text{cnt1}\)永远是相等的:都等于区间长度的一半。这就很方便我们用线段树维护我们需要的信息。
时间复杂度\(O(n\log 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;
}
给定一个偶数\(N\),现在蔡老板得到了一个由\([1,N]\)内的所有偶数构成的排列\(b[1..N/2]\)。
现在蔡老板又得到了一个数组\(a[1..N/2]\),其中\(a[i]=i\cdot 2?1\)。
蔡老板想知道,对于所有满足\(a\)和\(b\)都是它的子序列的\([1,N]\)的排列\(p\),\(p\)的逆序对数的最小值。
数据范围:\(1\leq N\leq 2\times 10^5\)。
逆序对数=偶数序列自身的逆序对数+奇数序列中每个数插入在偶数序列某个位置时产生的贡献。具体来说,插入在偶数序列某个位置时,产生的贡献就是偶数序列中该位置前面大于该奇数的数的数量+后面小于该奇数的数的数量。
考虑从小到大依次把每个奇数插入。每次都从对于该奇数最优的位置插入。这样的逆序对数一定是最少的。但是会不会出现:较小的奇数插在了较后面的位置,较大的奇数插在了较前面的位置,从而导致不满足“奇数序列是结果序列的一个子序列”这一要求呢?并不会。因为如果每次都选择最优的位置,会发现最优的插入位置是单调递增(可能非严格,但是没关系)的。
具体来说,共有\(\frac{n}{2}+1\)个可能的插入位置。我们用线段树维护每个插入位置的代价。从小到大考虑每个奇数。插入完一个奇数后,变成下一个奇数时,需要对线段树做的修改是:让一段后缀\(-1\),让一段前缀\(+1\):这样显然最优的位置单调递增。
时间复杂度\(O(n\log 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;
}
给定一棵\(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,m\leq 2500\)。
考虑每条边对答案的贡献。设这条边两侧的节点数分别为\(x,y\) (\(x+y=n\)),边权为\(w\)。考虑它对答案的贡献,我们可以枚举在\(x\)里有多少男生,多少女生,则贡献为:
如果对所有边暴力计算,总时间复杂度\(O(nm^2)\)。特判\(w=0\)的情况,可得\(70\)分。
考虑用前缀和来优化这个式子。具体来说,对每条边,我们先预处理出\(f,f_1,f_2\)三个数组:
对这三个数组做前缀和。然后枚举\(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;
}
YJC最近在研究炉石。今天他在研究关于其中三张卡牌的一个问题:
这三张卡牌描述如下:
现在,你的手牌中有足够多的香蕉和月火术以及一张亵渎。你正面临着你对手的一个巨大的场面,你需要清空这个场面。 这个场面上有\(n\)个随从,每个随从的生命值由输入给出。由于种种原因,你所在的这局游戏这三张卡牌的费用不一定是原来的费用,他们将在输入中被给定。 现在你想知道,为了清空这个场面,至少需要多少费用。
如果你完全没有看懂上面讲了什么,可以看下面的简要题意: 你有一个可重集合\(A\),\(A\)中的元素是正整数,当一个元素变为\(0\)时从集合中删除。 你可以花费\(p\)使一个元素\(+1\),花费\(q\)使一个元素\(?1\)。 你有一次机会花费\(r\)使得集合中所有元素\(?1\),若有元素因此被删除,则重复这个操作(不需要额外的花费)。 你的目标是最小化将集合变为空集的花费。
数据范围:\(n\leq 5000\),\(1\leq a[i]\leq 10^6\),\(0\leq p,q,r\leq 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;
}
YJC最近在研究世界杯,他拿一半财产压了德国队,另一半财产压了阿根廷队,结果可想而知。YJC表示非常angry,于是又开始研究博彩公司的盈利原理。
假设在世界杯决赛前,有\(n\)个人参与了赌博。第\(i\)个人认为法国队赢的概率是\(p[i]\),克罗地亚队赢的概率是\(1?p[i]\)。对于每一只球队,如果根据博彩公司给出的赔率第\(i\)个人的期望收益非负,则他会给这只球队下注\(a[i]\)元(设赔率为\(x\),某人下注了\(y\)元,如果他赢了可以返还\(x?y\)元,他输了则会返还\(0\)元。不论输赢,下注的钱均不返还)。如果两只球队期望收益均非负,则他会给两只球队各下注\(a[i]\)元。
现在博彩公司要决定法国队和克罗地亚队的赔率,使得在自身收益最小的胜负情况下的收益最大。
数据范围:\(1\leq n\leq 10^6\), \(0\leq a[i]\leq 100\), \(0\leq p[i]\leq 1\)。输入、输出精度均为\(6\)位小数。
考虑第\(i\)个人。设\(p_1(i)\)表示他认为法国队获胜的概率,\(p_2(i)\)表示他认为克罗地亚队获胜的概率,即:\(p_1(i)=p[i],p_2(i)=1-p[i]\)。那么,这个人会下注球队\(t\) (\(t\in\{1,2\}\)),当且仅当\(a[i]\cdot p_t(i)\cdot x\geq a[i]\),其中左边是他期望赢得的金额。把这个不等式化简,得:\(x\geq \frac{1}{p_t(i)}\)。
令\(f_t(i)=\frac{1}{p_t(i)}\),那么,第\(i\)个人会下注球队\(t\),当且仅当:\(x\geq f_t(i)\)。所以,对每个球队,有效的赔率取值只有\(n+1\)种(即小于所有\(f_t(i)\),或者刚好等于\(n\)个中的某个\(f_t(i)\))。
暴力的做法是枚举两个球队的赔率分别为多少,再计算收益。时间复杂度\(O(n^3)\)。
发现,收益对于两个球队的赔率是可以分别计算的。具体来说,对于球队\(t\),假设赔率为\(x\),那么:
求出\(K_t\)和\(Z_t\)后,我们枚举两个球队分别的赔率:\(x_i\), \(x_j\),则盈利为:\(\min\big(Z_1(x_i)-K_2(x_j),Z_2(x_j)-K_1(x_i)\big)\),分别代表了球队\(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[1\dots n],A_2[1\dots n],B_1[1\dots n],B_2[1\dots n]\)。求:\(\max_{i,j\leq n}\{\min(A_1[i]+B_1[j],A_2[i]+B_2[j])\}\)。
这里的\((A_1,A_2)\),可以看做一个\(\texttt{pair}\)型的数组\(A\),因为\(A_1,A_2\)出现时下标一样。\((B_1,B_2)\)同理。对\(A\)数组,按\(A_1\)从小到大排序。对\(B\)数组,按\(B_1\)从小到大排序。(也就是\(\texttt{pair}\)默认的比较方式)。
考虑二分答案\(\text{mid}\)。我们要判断是否存在一对\(i,j\),满足\(A_1[i]+B_1[j]\)和\(A_2[i]+B_2[j]\)均大于等于\(\text{mid}\)。
枚举\(A\)的下标\(i\)。那么,能够满足\(A_1[i]+B_1[j]\geq \text{mid}\)的\(j\),一定是\(B\)序列的一段后缀。我们只需要拿这个后缀里,\(B_2[j]\)最大的\(j\),看\(B_2[j]+A_2[i]\)是否\(\text{mid}\)即可。这段后缀的长度可以在枚举\(i\)时用 two pointers 维护。后缀最大的\(B_2\),可以预处理出来。
时间复杂度\(O(n\log n+n\log(\text{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;
}
YJC最近在研究数组,他认为如果数组的一个区间内不包含重复的元素,那么这个区间是一个优美的区间。
现在YJC弄到了一个interesting的数组,他想知道这个数组有多少个优美的区间。当然,他有时候也会觉得这个数组不够interesting,此时他会修改数组中的一个元素。
YJC发现在修改之后他不会算有多少个优美的区间了,于是他来向你求助。
数据范围:\(1\leq n\leq 10^5\), \(q\leq 2n\), \(1\leq a_i\leq n\)。(\(a_i\)是数组中的元素)
记\(\text{pre}[i]\)表示位置\(i\)上的数,在序列里上一次出现的位置;记\(\text{nxt}[i]\)表示位置\(i\)上的数,在序列里下一次出现的位置。像本题这种限制数在区间里出现次数的问题,往往可以转化为\(\text{pre}[i]\)和\(\text{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\)又需要大于\(\text{pre}[i]\)。所以,\(f_{i}=\max(f_{i-1},\text{pre}[i]+1)\)。有了这个递推式,再看求答案的式子,发现答案就等于\(\sum_{i=1}^{n}(i-\max_{j=1}^{j}\text{pre}[j])\),于是问题转化为维护\(\text{pre}\)数组的“前缀\(\max\)”之和。
一次修改操作,只会对不超过\(3\)个点的\(\text{pre}\)值产生影响,分别是:\(x\),原来的\(\text{nxt}[x]\),新的\(\text{nxt}[x]\)这三个位置。这几个位置是很容易求出的。我们可以开\(n\)个\(\texttt{set}\),记录每个值在哪些位置出现过。然后在\(\texttt{set}\)里二分,就能找到这几个要修改的位置。
于是问题转化为,维护一个序列,支持单点修改,询问“前缀最大值”之和。
考虑用线段树维护。对线段树上,每个节点\(p=[l,r]\),我们记录两个值:
难点在于如何\(\text{pushup}\),也就是合并左、右两个儿子。合并时,\(\text{mx}\)直接取两个儿子的较大值即可。左儿子的\(\text{sum}\),可以直接贡献到当前节点上。而右儿子的里,需要先对左儿子的最大值\(\text{mx}[\text{left_son}]\)取\(\max\),再累加到\(\text{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(n\log^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;
}
先不管问题。我们尝试维护出每个时刻每个人的巨轮是谁。直观的做法是用一个二维数组,存pair<long long,int>
类型,分别表示两人之间的消息数量和最近一次发消息的时间。我们需要支持单点修改,并维护这个数组每一行的最大值。直观的做法是每行开一个线段树,但这样空间会超限。改成动态开点线段树就好了:因为修改次数是\(O(n)\)的,每次修改只会在线段树上新增\(O(\log n)\)个节点,所以空间复杂度是\(O(n\log n)\),可以接受。
维护出每个人的巨轮后,考虑回答询问。发现每个时刻,只有不超过\(4\)个人的巨轮会变化(新加入的一对人,和刚刚过期的一对人)。我们说一个人的“状态”是指,他是否是单向巨轮。那么,如果\(x\)这个人的巨轮变化了,那么我们只需要更新:(1) 原来\(x\)的巨轮;(2) \(x\);(3) \(x\)的新巨轮;这三个点的状态。容易发现,其他点的状态不会改变。所以可以\(O(1)\)完成所有更新。
时间复杂度\(O(n\log 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;
}
首先,给定一棵树,如何求出所有点对距离和?朴素做法是枚举两个点,求一遍距离,时间复杂度\(O(n^3)\)。如果求距离用\(O(1)\)LCA的方法求,则优化到\(O(n^2)\)。换个角度,累加“每条边的贡献”,例如,考虑\(u\)和\(fa(u)\)之间的边,则答案为\(\sum_{u\neq 1}\text{size}_u\cdot (n-\text{size}_u)\),于是只需要求出每个点的\(\text{size}\)即可,时间复杂度\(O(n)\)。本题就是在这种考虑“每条边贡献”的思想的基础上完成的。
回到本题,也就是这个“树套树”的模型中。我们把贡献分为三类:
第3类贡献比较麻烦。我们来仔细讨论。
依次考虑每棵小树。对当前小树,先以\(1\)为根,把它看成一棵有根树。然后转化为,有一棵树,然后给定一些二元组\((x,y)\),表示在节点\(x\)上,挂了\(y\cdot n\)个点。
我们记小树节点\(u\)子树内的点数为\(\text{in}(u)\),子树外的点数为\(\text{out}(u)\)。初始时,只考虑了当前小树里的点(没有考虑二元组挂上去的点),也就是\(\text{in}(u)=\text{size}_u,\text{out}(u)=n-\text{size}_u\)。请注意,这里\(u\neq 1\),因为\(1\)节点和父亲之间没有边,自然也不会产生贡献。然后我们依次考虑每个二元组\((x,y)\),把它挂上去时,会新增的贡献数是:
然后用二元组\((x,y)\)来更新每个节点的\(\text{in},\text{out}\):
可以看出,我们要支持,(1) 给所有祖先加,(2) 给所有其他节点加。而“给其他节点加”,可以转化为先给所有节点加,然后再给祖先减掉。所以只需要支持对所有祖先加,对所有祖先求和即可。可以用树链剖分+线段树实现。
虽然每个小树上可能挂多个二元组,但二元组的本质就是大树上的边,所以总数只有\(O(m)\)个。每次树链剖分+线段树操作的复杂度是\(O(\log^2n)\)的,所以总时间复杂度\(O(m\log^2n)\)。
有一个小问题是,每考虑完一个小树后,要清空线段树。准确来说不是清空,是还原成\(\text{in}(u)=\text{size}_u,\text{out}(u)=n-\text{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;
}
这种类似于全排列的搜索,往往都指向一个优化:状压DP。
设\(dp[mask]\)表示已经打败了\(mask\)里这些怪物,剩下的血量的最大值。除血量外,攻击、防御、魔防值都是确定的,可以顺便维护出来。
但是这样会遇到一个问题:什么时候取宝石。我们发现,一打完立即取走宝石并不一定是最优的,因为有“模仿怪”这种东西存在。所以这个状态要改一改。设\(dp[s_1][s_2]\)表示打败了\(s_1\)里面这些怪物,取走了\(s_2\)里这些宝石,剩余血量的最大值。显然,\(s_2\)一定是\(s_1\)的子集,所以总状态数是\(3^n\)。在具体实现时,我们不开二维数组,而是用三进制状态来表示:未被打败、被打败了但没有取走宝石、取走了宝石,这三种状态。
时间复杂度\(O(3^n\cdot 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;
}
设\(s=\sum c_i\)。
首先,抽卡的总次数是有限的。当总卡数达到\(4s+3n\)时,无论抽到的是什么卡,都必能兑换所有服装。这是因为,每\(4\)张同一种类的卡,就能兑换任意一种服装。而为了避免卡被模\(4\)的余数浪费掉,所以预先安排\(3n\)张卡,专门用于被余数浪费。如果你不能理解\(+3n\),可以想一想\(c=\{0,0,1\}\),抽到的卡为\(\{2,2,0\}\)的情况。
现在,考虑手上抽到的卡,把每种卡被抽到的数量的序列,称为一个“状态”。那么,如果当前手上抽到的卡,已经能够获得\(n\)件衣服,我们称这个状态为“不好”的。否则,也就是无法获得\(n\)件衣服时,我们称这样的状态是“好的”。
题目问,第一次达到“不好的”状态,所经过的期望操作次数。就相当于问,经过的“好的”状态的数量的期望。另外,显然,“好的”状态之间,是一个严格的树形结构,不会出现环(因为每个状态比它的父亲多一张卡)。所以,每个状态最多只会被经过\(1\)次。因此,根据期望的线性性,经过的“好的”状态的数量的期望,就等于经过每个“好的”状态的概率之和(其实本质是概率\(\times 1\)之和,因为最多只会经过\(1\)次)。
可以用DP求所有“好的”状态的概率之和。依次考虑每种卡。假设当前考虑了前\(i\)种卡。我们要知道前\(i\)种卡,能为后面贡献多少张卡,记为\(k\)。这里的“贡献”,就是指每四张相同类型的卡换任意一张其他卡。例如,如果前\(i\)种卡里,某一种卡需要\(c\)张,而实际抽到了\(t\)张,则会令贡献值\(k\)增加\(\lfloor\frac{t-c}{4}\rfloor\)。当然,如果前\(i\)种卡,自己都没有抽满,需要后面的卡来贡献它们,则\(k\)为负数。
于是可以设计一个DP状态:设\(dp[i][j][k]\)表示考虑了前\(i\)种卡,总共选出了\(j\)张卡,贡献值为\(k\)的方案数。转移时,枚举第\(i\)种卡选了\(t\)张。如果\(t<c_i\),则新的\(k‘=k-(c_i-t)\);否则\(k‘=k+\lfloor\frac{t-c_i}{4}\rfloor\)。
在转移时,根据新增的卡数\(t\),我们乘上系数\(\frac{(p_i)^t}{t!}\)。在最后统计答案时,如果选出的总卡数为\(J\),我们再乘上\(J!\)。最后就相当于乘以了\(\frac{J!}{\prod(t!)}\),这也就是含重复元素的排列计数。
分析时间复杂度。\(i\leq n,0\leq j\leq 4s+3n,-s\leq k\leq 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;
}
标签:枚举 event 还原 就是 二分答案 它的 相同 不等式 个人
原文地址:https://www.cnblogs.com/dysyn1314/p/12966906.html