新高一暑假集训随记1

8.11

上午

回来第一天上午让我们自己补题。

@tkdqmx 找了一道黄来做拼尽全力一个小时没有战胜,狠狠嘲笑。

然后我随机跳题跳到一道蓝拼尽全力 1h50min 打挂了最后抄了 tj,被狠狠嘲笑。

下午

DP 复习。

离线思考一个小时发现三道假了一道半。

P10652 [ROI 2017] 前往大都会 (Day 1)

第二问明示要把最短路图建出来,所以先建最短路图。

一开始想着对每个点记录不同这个点能换乘的线路的答案以及一个最大值,总状态数是 \(O(n+m)\) 的,但是我没写,不知道假没假。

最后还是写的斜优。

注意建出最短路图后,一条线路会被断开,这个时候需要将其拆成多条线路重新编号,因为这些断开的线路在之后已经没有关联了。

按 dis(或者拓扑序)排序去除后效性,有 \(dp_i=dp_j+(dis_i-dis_j)^2\),然后就是很套路的斜优了。

最后调了半个小时是因为没按 dis 排序,寄。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const ll N=1e6+5,INF=0x3f3f3f3f3f3f3f3f;
struct edge{ll to,w,c;};
vector<edge>e[N];
vector<ll>dis;
vector<bool>vis;
struct node{
	ll now,dis;
	friend bool operator<(node x,node y){return x.dis>y.dis;}
};
priority_queue<node>q;
template<typename T>
void Dijkstra(int nc,int st,vector<edge>* g,vector<T>& dis){	
	dis.assign(nc+1,INF);
	vis.assign(nc+1,0);
	dis[st]=0;q.push({st,0});
	while(!q.empty()){
		int now=q.top().now;q.pop();
		if(vis[now])	continue;
		vis[now]=1;
		for(auto it:g[now]){
			if(dis[it.to]>dis[now]+it.w){
				dis[it.to]=dis[now]+it.w;
				q.push({it.to,dis[it.to]});
			}
		}
	}
}
struct LC_segment_tree{
	struct line{
		ll k,b;
		ll calc(ll x){return k*x+b;}
	}li[N];
	int cnt=0,siz=0,rt[N];
	struct segment_tree_node{int ls,rs,lazy;}t[N<<1];
	bool cmp(line a,line b,ll x){return a.calc(x)>b.calc(x);}
	void clear(){cnt=siz=0;}
	inline int new_node(){t[++siz]={0,0,0};	return siz;}
#define ls(p) t[p].ls
#define rs(p) t[p].rs
#define mid ((l+r)>>1)
	void change(int& p,int l,int r,int num){
		if(!p)	p=new_node();
		if(cmp(li[num],li[t[p].lazy],mid))	swap(num,t[p].lazy);
		if(cmp(li[num],li[t[p].lazy],l))	change(ls(p),l,mid,num);
		if(cmp(li[num],li[t[p].lazy],r))	change(rs(p),mid+1,r,num);
	}
	ll query(int p,int l,int r,int pos){
		if(!p)	return -INF;
		ll ret=li[t[p].lazy].calc(pos);
		if(l==r)	return ret;
		else{
			if(pos<=mid)	return max(ret,query(ls(p),l,mid,pos));
			else	return max(ret,query(rs(p),mid+1,r,pos));
		}
	}
	void add(ll k,ll b,vector<int>v){
		li[++cnt]={k,b};
		for(auto it:v)	change(rt[it],0,1e9,cnt);
	}
}T;
bool cmp(int x,int y){return dis[x]<dis[y];}
ll n,m,p[N],dp[N],c;
vector<int>in[N],num[N];
map<PII,bool>mp[N];
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>m;T.li[0]={0,-INF};
	for(int i=1,k,lst;i<=m;i++){
		cin>>k>>lst;num[i].push_back(lst);
		for(int j=1,t,to;j<=k;j++)	cin>>t>>to,e[lst].push_back({to,t,i}),num[i].push_back(to),lst=to;
	}
	Dijkstra(n,1,e,dis);
	for(int i=1;i<=n;i++){
		for(auto it:e[i]){
			if(dis[i]+it.w==dis[it.to])	mp[it.c][{i,it.to}]=1;
		}
	}
	for(int i=1;i<=m;i++){
		for(int j=0,lst=-1;j<num[i].size();j++){
			if((j+1==(int)num[i].size()||!mp[i][{num[i][j],num[i][j+1]}])&&lst!=-1){
				c++;
				for(int k=lst;k<=j;k++)		in[num[i][k]].push_back(c);
				lst=-1;
			}else if(mp[i][{num[i][j],num[i][j+1]}]){
				if(lst==-1)	lst=j;
			}
		}
	}
	for(int i=1;i<=n;i++)	sort(in[i].begin(),in[i].end()),in[i].resize(unique(in[i].begin(),in[i].end())-in[i].begin());
	for(int i=1;i<=n;i++)	p[i]=i;sort(p+1,p+n+1,cmp);
	for(int i=1;i<=n;i++){
		for(auto it:in[p[i]])	dp[p[i]]=max(dp[p[i]],T.query(T.rt[it],0,1e9,dis[p[i]])+dis[p[i]]*dis[p[i]]);
		T.add(-2*dis[p[i]],dp[p[i]]+dis[p[i]]*dis[p[i]],in[p[i]]);
	}
	cout<<dis[n]<<" "<<dp[n]<<"\n";
}

P6173 [USACO16FEB] Circular Barn P

破环为链,发现 \(w(l,r)\) 满足四边形不等式

\(w(l,r)=\sum_{i=l}^r(i-l)a_i\)

有: \(\\w(l-1,r+1)-w(l-1,r)=(r-l+2)a_{r+1} \\ w(l,r+1)-w(l,r)=(r-l+1)a_{r+1}\)

\(w(l-1,r+1)+w(l,r)-w(l-1,r)-w(l,r+1)=a_{r+1}>0\)

枚举起点,分治优化 \(O(nk\log n)\) 直接过。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const ll N=1005,K=8,INF=0x3f3f3f3f3f3f3f3f;
ll n,k,r[N<<1],w[N<<1][N<<1],dp[K][N<<1],ans=INF;
void solve(int k,int l,int r,int lv,int rv){
	if(l>r)	return;
	int mid=(l+r)>>1,p;dp[k][mid]=INF;
	for(int i=lv;i<=min(mid,rv);i++){
//		cerr<<dp[k-1][i-1]+w[i][mid]<<"\n";
		if(dp[k-1][i-1]+w[i][mid]<dp[k][mid])	dp[k][mid]=dp[k-1][i-1]+w[i][mid],p=i;
	}
	solve(k,l,mid-1,lv,p),solve(k,mid+1,r,p,rv);
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>k;
	for(int i=1;i<=n;i++)	cin>>r[i],r[n+i]=r[i];
	for(int i=1;i<=n*2;i++){
		for(int j=i;j<=n*2;j++)	w[i][j]=w[i][j-1]+r[j]*(j-i);
	}
	for(int i=1;i<=n;i++){
		for(int j=i-1;j<=i+n-1;j++){
			for(int l=0;l<=k;l++)	dp[l][j]=INF;
		}
		dp[0][i-1]=0;
		for(int l=1;l<=k;l++)	solve(l,i,i+n-1,i,i+n-1);
		ans=min(ans,dp[k][i+n-1]);
	}
	cout<<ans<<"\n";
}

但是神犇 @paper_ 给出了一个 \(O(n\log n)\) 的做法。

他说随着起点向后移,\(k\) 个决策点也具有决策单调性,所以可以先选一个起点做决策单调性,然后对决策点做一遍大的决策单调性(其实我是没听懂的)就行。

但是 \(0\) 人写了这个做法。

仙人掌

给你一个二元组 \((a_i,b_i)\) 序列,求最小的区间 \([l,r]\) 的长度,其中 \([l,r]\) 满足这里面的每个二元组选或不选,使得 \(\sum_{a_i} = w\)\(\sum b_i \le k\)。如果无解,请输出 -1。

\(1 \le a_i \le w \le 5 \times 10^3, 1 \le k \le 10^9, 1 \le s \le 10^4\)

双栈维护队列的 trick,可以加速支持快速合并左右区间答案但难支持撤销的操作,如本题的重量背包价值 \(\min\) 问题。

第一维可以背包满足,DP 内记录价值 \(\min\) 发现对于合法区间 \(r\)\(l\) 增加不减,所以可以双指针维护。

但是这个背包明显不支持退背包的操作,所以可以用双栈维护队列。

站内每个元素记录当前到栈底所有元素的背包结果(即 DP 数组)。加入的时候以之前的栈顶 \(O(w)\) 更新现在的栈顶即可。

\(r\) 右移时,加入一个新元素,则直接塞进右栈更新,\(l\) 右移时直接弹出左栈,特别的,如果左栈空了,则直接将右栈所有元素弹出,塞入左栈。

查询的时候直接合并两边栈顶的 DP 数组即可。

每个元素至多进栈、出栈各两次,所以复杂度是正确的。

时间复杂度 \(O(nw)\)

注意本题需要提前在两个栈内塞入空背包,确保能够更新。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
using VLL=vector<ll>;
const ll N=1e4+5,C=5e3+5,INF=0x3f3f3f3f3f3f3f3f;
stack<pair<int,VLL>>L,R;
ll n,w,k,a[N],b[N],ans=INF;
VLL add(VLL x,int y){
	VLL ret=x;
	for(int i=w;i>=a[y];i--)	ret[i]=min(ret[i],ret[i-a[y]]+b[y]);
	return ret;
}
bool check(){
	for(int i=0;i<=w;i++){
		if(L.top().se[i]+R.top().se[w-i]<=k)	return 1;
	}
	return 0;
}
void push(stack<pair<int,VLL>>& st,int x){
	if(st.empty()){//塞入空背包
		VLL tmp(w+1,INF);tmp[0]=0;
		st.push({x,tmp});
	}else{
		VLL tmp=add(st.top().se,x);
		st.push({x,tmp});
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>w>>k;
	for(int i=1;i<=n;i++)	cin>>a[i]>>b[i];
	push(L,0),push(R,0);
	for(int l=1,r=0;l<=n;l++){
		while(r<n&&!check())	push(R,++r);
//		cerr<<l<<" "<<r<<"\n";
		if(check())	ans=min(ans,r-l+1ll);
		if(!L.top().fi){
			while(R.top().fi)	push(L,R.top().fi),R.pop();
		}
		L.pop();
	}
	cout<<(ans==INF?-1:ans)<<"\n";
}

8.12

还是被DP狠狠制裁的一天。

The Hanged Man

倒吊人说是。

树上独立集背包权值和最大化。

首先有个简单的 \(O(nm^2)\) DP,就不多赘述。

考虑到转移只与父节点的状态有关,所以可以使用这样一个 trick:

对树进行重链剖分,先遍历轻子树在遍历重字数生成 dfn。

考虑按 dfn DP,当 dfn 增加 \(1\) 时,要么由上一个节点直接走向儿子,要么由上一个节点跳若干条重链然后走向自己的儿子。

因为对于某个结点,其所在重链上由其祖先延伸出的轻边可抵达的结点 dfn 一定更小,所以如果要跳重链,就一定会跳完一整条。

则下一个结点的父亲一定是当前结点或当前结点所能跳到的重链的链头的父亲的儿子。

所以对于每一个结点,我们只需要记录这个结点自己及其所能跳到的重链链头的父亲即可。

对这些状态状压,复杂度为 \(O(2^{\log n})=O(n)\)

然后就可以根据前一个结点所有关键点的选择情况转移到当前结点了。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
#define int ll
const int N=55,C=5005;
int T,cnt,n,m,now=1,pre,a[N],b[N];
int dfn[N],rdfn[N],siz[N],wc[N],fa[N],top[N],dfc;
vector<int>e[N],chain[N];
struct node{int v,num;}dp[2][N][C];
void merge(node& x,node y){
	if(x.v<y.v)	x=y;
	else if(x.v==y.v)	x.num+=y.num;
}
void dfs1(int now,int f){
	fa[now]=f;siz[now]=1;
	for(auto it:e[now]){
		if(it!=f){
			dfs1(it,now);
			siz[now]+=siz[it];
			if(siz[it]>siz[wc[now]])	wc[now]=it;
		}
	}
}
void dfs2(int now,int f,int Top){
	dfn[now]=++dfc,rdfn[dfc]=now,top[now]=Top;
	for(auto it:e[now]){
		if(it!=f&&it!=wc[now])	dfs2(it,now,it);
	}
	if(wc[now])	dfs2(wc[now],now,Top);
}
void init(){
	for(int i=1;i<=n;i++){
		int tmp=i;chain[i].push_back(i);
		while(tmp=fa[top[tmp]])	chain[i].push_back(tmp);
	}
	for(int j=0;j<2;j++){
		for(int k=0;k<=m;k++)	dp[now][j][k]={0,0};
	}
	dp[now][0][0]={0,1};dp[now][1][a[rdfn[1]]]={b[rdfn[1]],1};
	swap(now,pre);
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>T;
	while(T--){
		cin>>n>>m;dfc=0;
		for(int i=1;i<=n;i++)	cin>>a[i]>>b[i],e[i].clear(),chain[i].clear(),wc[i]=0;
		for(int i=1,u,v;i<n;i++){
			cin>>u>>v;
			e[u].push_back(v);
			e[v].push_back(u);
		}
		dfs1(1,0),dfs2(1,0,1);
		init();
		for(int i=2;i<=n;i++){
			int it=rdfn[i],p=rdfn[i-1],s=chain[it].size(),ps=chain[p].size(),fap=-1;
			vector<int>to(ps,-1);
			for(int j=0;j<(1<<s);j++){
				for(int k=0;k<=m;k++)	dp[now][j][k]={0,0};
			}
			for(int j=0;j<ps;j++){
				int tmp=chain[p][j];
				if(tmp==fa[it])	fap=j;
				for(int k=0;k<s;k++){
					if(chain[it][k]==tmp){
						to[j]=k;
						break;
					}
				}
			}//这里是记录前一个结点的关键点与这一个结点关键点的位置对应关系,fap是当前结点父亲的对应位置
			for(int P=0;P<(1<<ps);P++){
				int S=0;
				for(int j=0;j<ps;j++){
					if((P&(1<<j))&&to[j]!=-1)	S|=(1<<to[j]);	
				}
				for(int j=0;j<=m;j++){
					if(dp[pre][P][j].num){
						merge(dp[now][S][j],dp[pre][P][j]);
						if(!(P&(1<<fap))&&j+a[it]<=m)	merge(dp[now][S|1][j+a[it]],{dp[pre][P][j].v+b[it],dp[pre][P][j].num});//注意这里用P来判断父亲状态
					}
				}
			}
			swap(now,pre);
		}
		cout<<"Case "<<++cnt<<":\n";
		for(int i=1;i<=m;i++){
			node ans={0,0};
			for(int S=0;S<(1<<(chain[rdfn[n]].size()));S++)	merge(ans,dp[pre][S][i]);
			cout<<ans.num<<" ";
		}
		cout<<"\n";
	}
}

旅行

wyf 非常喜欢旅行,现在他来到了一个神奇的国家,这个国家有 \(n\) 个城市,城市之间通过 \(n-1\) 条双向道路相连,且任意两个城市通过唯一的简单路径互相到达。

wyf已经为自己在这个国家规划了一些旅行,每次旅行他都会选择一个出发的城市 \(S\) 和到达的城市 \(T\)。在此之前他为这个国家的每一座城市都评估了一个美丽值,而在从 \(S\) 旅行到 \(T\) 的期间,他可以选择一些城市认真游览一番(当然他不会绕路游览某个城市)。

他的幸运数字是 \(K\),而他希望自己能够一直幸运下去,所以他希望自己一次旅行所停留游览的城市的美丽值之和是 \(K\) 的整数倍。

对于每一次你能不能告诉他他有多少种游览的方案。

\(1\le n,q\le2\times 10^5,1\le K\le 50\)

计数可以使用模意义下背包解决,路径问题由于背包可差分性不强,考虑离线下来使用点分治解决以减少背包合并次数,

点分治时,如果路径两端点不在该点同一子树内,则 dfs 处理背包,然后 \(O(K)\) 合并求答案即可。

如果路径两端点在同一子树内,则直接将询问下传到该子树的重心之后解决。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=2e5+5,C=55,mod=998244353;
ll n,k,m,a[N],dp[N][C],f[C],in[N],ans[N],col;
int now_siz,rt,siz[N],ma[N];
bool vis[N];
vector<int>e[N];
struct query{int u,v,num;};
vector<query>q[N],tmp[N];
void get_rt(int now,int fa){
	siz[now]=1,ma[now]=0;
	for(auto it:e[now]){
		if(it!=fa&&!vis[it]){
			get_rt(it,now);
			siz[now]+=siz[it];
			ma[now]=max(ma[now],siz[it]);
		}
	}
	ma[now]=max(ma[now],now_siz-siz[now]);
	if(ma[now]<ma[rt])	rt=now;
}
void dfs(int now,int fa,int c){
	in[now]=c;
	for(int i=0;i<k;i++)	dp[now][(i+a[now])%k]=(dp[fa][(i+a[now])%k]+dp[fa][i])%mod;
	for(auto it:e[now]){
		if(it!=fa&&!vis[it])	dfs(it,now,c);
	}
}
void calc(int now){
	for(int i=0;i<k;i++)	dp[now][i]=0;dp[now][0]=1;
	for(auto it:e[now]){
		if(!vis[it]){
			++col;
			dfs(it,now,col);
		}
	}
	for(auto it:q[now]){
		if(in[it.u]!=in[it.v]){
			for(int i=0;i<k;i++)	f[(i+a[now])%k]=(dp[it.v][(i+a[now])%k]+dp[it.v][i])%mod;
//			cerr<<it.u<<" "<<it.v<<"\n";
//			for(int i=0;i<k;i++)	cerr<<dp[it.u][i]<<" ";cerr<<"\n";
//			for(int i=0;i<k;i++)	cerr<<f[i]<<" ";cerr<<"\n";
			for(int i=0;i<k;i++)	ans[it.num]=(ans[it.num]+dp[it.u][i]*f[(k-i)%k]%mod)%mod;
		}else	tmp[in[it.u]].push_back(it);
	}
	q[now].clear();
}
void dfz(int now){
//	cerr<<now<<"\n";
	vis[now]=1,calc(now);
	for(auto it:e[now]){
		if(!vis[it]){
			now_siz=siz[it],rt=0,get_rt(it,now),get_rt(rt,now);
			q[rt]=tmp[in[rt]];tmp[in[rt]].clear();
			dfz(rt);
		}
	}
}
int main(){
//	freopen("travel1.in","r",stdin);
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>k;ma[0]=0x3f3f3f3f;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	for(int i=1;i<=n;i++)	cin>>a[i];
	now_siz=n,get_rt(1,0),get_rt(rt,0);
	cin>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		if(u==v)	ans[i]=1+(a[u]%k==0);
		else	q[rt].push_back({u,v,i});
	}
	dfz(rt);
	for(int i=1;i<=m;i++)	cout<<ans[i]<<"\n";
}

P6383 『MdOI R2』Resurrection

\(n\) 为根,考虑儿子编号小于祖先的性质如何利用。

我们发现,断边之后,只会是儿子向祖先连边,因为当前连通块的根编号最大,则当前联通块的根必然会被连接。

同时,连边不会出现交叉,即使得存在祖先关系的四个点 \(x<y<z<w\)\(x\)\(z\) 连边,\(y\)\(w\) 连边,手玩一下就理解了。

每个节点会向自己的某个祖先节点连边,并且连边不能交叉。我们要计算这样的方案数。

这时候可以考虑 DP 求方案数:令 \(dp_{i,j}\) 表示,当前算到了结点 \(i\),结点 \(i\)\(j\) 种连接祖先的选择。

有转移:

\[dp_{i,j}=\prod_{s\in son}\sum_{k=1}^{j+1}dp_{s,k} \]

因为 \(i\) 的儿子连边必定有 \(u\) 这个选择,所以枚举范围是 \([1,j+1]\)

前缀和优化即可做到 \(O(n^2)\)

最后答案就是 \(dp_{n,0}\)

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=3005;
const int mod=998244353;
vector<int>e[N];
ll n,dp[N][N];
void DP(int now,int fa){
	for(int i=1;i<=n;i++)	dp[now][i]=1;dp[now][0]=(now==n);
	for(auto it:e[now]){
		if(it!=fa){
			DP(it,now);
			for(int i=0;i<=n;i++)	dp[now][i]=dp[now][i]*dp[it][i+1]%mod;
		}
	}
	for(int i=1;i<=n;i++)	dp[now][i]=(dp[now][i-1]+dp[now][i])%mod;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n;
	for(int i=1,x,y;i<n;i++){
		cin>>x>>y;
		e[x].push_back(y),e[y].push_back(x);
	}
	DP(n,0);
	cout<<dp[n][0]<<"\n";
}

JOISC 2012 kangaroo

澳洲猴和澳洲袋鼠是好朋友,他们经常在一起交流拳♂击,摔♂跤的经验。

众所周知,澳洲袋鼠的胸前有一个袋子,我们认为一个澳洲袋鼠的体积为 \(A_i\),那么它的袋子的大小就是 \(B_i\),显然 \(B_i\) 是严格小于 \(A_i\) 的,当然如果有东西装在这个袋鼠的袋子里面,那么袋鼠的体积仍然是 \(A_i\)

某一次,澳洲猴在摔♂跤比赛中赢了袋鼠,于是,袋鼠答应澳洲猴来做一个游戏。

这个游戏是这样的:全程都是澳洲猴操作,每次他可以选择一个袋鼠 \(i\) 放到袋鼠 \(j\) 的袋子里,但是这要满足:

1、袋鼠 \(i\) 目前不在其他袋鼠的袋子里;

2、袋鼠 \(j\) 目前袋子里没有其他袋鼠;

3、\(A_i < B_j\)

澳洲猴每次会将这 \(n\) 个袋鼠操作到不能操作为止,它想求出最终状态一共有多少可能,由于数字很大,请模 \((1e9+7)\) 输出。

由于 \(A_i>B_i\) 所以我们可以将 \(A,B\) 分别从大到小排序,不用担心一个袋鼠会把自己装进去,则对于每一个 \(A_i\) 我们可以找到一个最大 \(p_i\) 使得 \(B_{p_i}>A_i\),容易发现排序后 \(p_i\) 单调不减。

考虑一个 DP,令 \(dp_{i,j,k}\) 表示当前考虑到第 \(i\) 个袋鼠,前 \(j+k\) 个袋鼠中,有 \(j\) 个袋鼠已经被装入另一个袋鼠,有 \(k\) 个袋鼠需要装入袋鼠使得不能再进行操作。

则我们有以下转移。

  1. 不管第 \(i\) 个袋鼠,那么前 \(p_i\) 个袋鼠必须装入另一个袋鼠,不然就可以把第 \(i\) 个袋鼠装入,不满足条件,即:\(dp_{i,j,p_i-j}\overset{+}{\leftarrow}dp_{i,j,k}\)
  2. 将第 \(i\) 个袋鼠装入需要装入袋鼠的 \(k\) 个袋鼠之中:\(dp_{i,j+1,k-1}\overset{+}{\leftarrow}dp_{i,j,k}\times k\)
  3. 将第 \(i\) 个袋鼠装入没有装入袋鼠且不一定需要装入袋鼠的袋鼠里,这样的袋鼠有 \(p-j-k\) 个,即:\(dp_{i,j+1,k}\overset{+}{\leftarrow}dp_{i,j,k}\times(p-j-k)\)

答案即为 \(\sum \limits_{i=0}^{n}dp_{n,i,0}\)

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=305,mod=1e9+7;
ll n,a[N],b[N],dp[2][N][N],ans,now=1,pre;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++)	cin>>a[i]>>b[i];
	sort(a+1,a+n+1);reverse(a+1,a+n+1);
	sort(b+1,b+n+1);reverse(b+1,b+n+1);
	dp[pre][0][0]=1;
	for(int i=1,p=0;i<=n;i++){
		while(p<n&&a[i]<b[p+1])	p++;
		for(int j=0;j<=n;j++){
			for(int k=0;k<=n;k++)	dp[now][j][k]=0;
		}
		for(int j=0;j<i;j++){
			for(int k=0;j+k<=p;k++){
				dp[now][j][p-j]=(dp[now][j][p-j]+dp[pre][j][k])%mod;
				if(k)	dp[now][j+1][k-1]=(dp[now][j+1][k-1]+dp[pre][j][k]*k%mod)%mod;
				dp[now][j+1][k]=(dp[now][j+1][k]+dp[pre][j][k]*(p-j-k)%mod)%mod;
			}
		}
		swap(now,pre);
	}
	for(int i=0;i<=n;i++)	ans=(ans+dp[pre][i][0])%mod;
	cout<<ans<<"\n";
}

折半警报器/二进制警报器

折半警报器二进制警报器

用于解决多点加,求和警报问题。

P7603 [THUPC 2021] 鬼街

板子,\(k=\omega(A_i)\)

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5,LG=61;
vector<ll>pos[N],warn[N][LG],fac[N],ans;
ll lv[N],lim[N],cnt,lst;
bool vis[N];
ll n,q,a[N];
ll calc(int x,ll shd){
	ll sum=0;
	for (auto it:pos[x])	sum+=a[it]+(1ll<<shd)-(((1ll<<shd)-1)&a[it])-1;
	return sum;
}
void add(int x,ll v){
	ll mask=a[x]^(a[x]+v);a[x]+=v;
	for(int i=0;(1ll<<i)<=v||(mask&(1ll<<i));i++){
//		cerr<<"WARN:"<<x<<" "<<i<<"\n";
		vector<ll> tmp(0);tmp.swap(warn[x][i]);
		for(auto it:tmp){
			if(!vis[it]&&lv[it]==i){
				if(calc(it,0)>=lim[it])	vis[it]=1,ans.push_back(it);
				else{
					while(lv[it]&&calc(it,lv[it])>=lim[it])	lv[it]--;
					if(lv[it]==i)	warn[x][i].push_back(it);
					else{
						for(auto to:pos[it])	warn[to][lv[it]].push_back(it);
					}
				}
			}
		}
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin>>n>>q;
	for(int i=2;i<=n;i++){
		if(!fac[i].size()){
			for(int j=i;j<=n;j+=i)	fac[j].push_back(i);
		}
	}
	for(ll i=1,opt,x,v;i<=q;i++){
		cin>>opt>>x>>v;v^=lst;
		if(opt==1){
			cnt++;pos[cnt]=fac[x];
			if(!v)	ans.push_back(cnt);
			else{
				lim[cnt]=calc(cnt,0)+v;
				lv[cnt]=60;
				while(lv[cnt]&&calc(cnt,lv[cnt])>=lim[cnt])	lv[cnt]--;
				for(auto to:pos[cnt])	warn[to][lv[cnt]].push_back(cnt);
			}
		}else{
			for(auto it:fac[x])	add(it,v);
			cout<<(lst=ans.size())<<" ";
			sort(ans.begin(),ans.end());
			for(auto it:ans)	cout<<it<<" ";
			cout<<"\n";ans.clear();
		}
	}
}
posted @ 2025-08-12 17:06  -MornStar-  阅读(71)  评论(1)    收藏  举报