JSOI2014 总结

[AHOI2014/JSOI2014]宅男计划

题目链接

可以发现买来食物可以维持的天数关于叫外卖的次数是一个单峰函数,这个可以打表或是另写一个程序判断。所以使用三分法寻找峰值。根据三分出来的次数计算天数可以使用贪心策略,如果在保质期内就买最便宜的食品,一些细节也需要特判。

#include <cstdio>
#include <algorithm>
typedef long long ll;
inline ll rd(){
	ll x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
inline ll min(ll x,ll y){return x<y?x:y;}
const int N=202;
int n,top;
ll m,f,ans;
struct node{
	ll p,s;
	bool operator < (const node &y)const{
		return s<y.s;
	}
}a[N],b[N];
inline ll calc(ll t){
	ll res=m-f*t,now=0,x,ans=0;
	if(res<0)return 0;
	for(int i=1;i<=n;i++){
		x=min(b[i].s-now,res/(b[i].p*t));
		now+=x,ans+=x*t,res-=t*x*b[i].p;
		if(now<b[i].s){
			ans+=res/b[i].p;
			break;
		}
	}
	return ans;
}
int main(){
	m=rd(),f=rd(),n=rd();
	for(int i=1;i<=n;i++)a[i].p=rd(),a[i].s=rd()+1;
	std::sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		while(top&&a[i].p<=b[top].p)top--;
		b[++top]=a[i];
	}
	n=top;
	ll l=1,r=m/(f+b[1].p);
	while(l<=r){
		ll lmid=l+(r-l)/3,rmid=r-(r-l)/3;
		ll lx=calc(lmid),rx=calc(rmid);
		if(lx<rx)ans=rx,l=lmid+1;
		else ans=lx,r=rmid-1;
	}
	printf("%lld
",ans);
	return 0;
}

[AHOI2014/JSOI2014]骑士游戏

题目链接

有两种方法彻底杀死一个怪兽,即直接用法术攻击,或者用普通攻击,并杀死它死后产生的所有怪兽。设 (f_i) 为杀死一个怪兽的最小体力,(D_i) 为使用普通攻击后产生怪兽的集合,则:

[f_i=min{s_i,k_i+sumlimits_{jin D_i}f_j} ]

但是直接 (dp) 显然是不行的,因为题目所给的关系是可能存在环的。所以我们借助 (SPFA) 的松弛的形式,从 (i)(jin D_i) 连边去跑 (SPFA),使得一个点可以重复进入队列,从而可以得到最优解。

#include <cstdio>
#include <queue>
typedef long long ll;
inline ll rd(){
	ll x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
const int N=1000002;
int n;
ll a[N],dis[N];
struct Edge{
	int to,next;
}edge[N],redge[N];
int head[N],cnt,rhead[N],rcnt;
inline void add(int f,int t){
	edge[++cnt].next=head[f];
	edge[cnt].to=t;
	head[f]=cnt;
}
inline void radd(int f,int t){
	redge[++rcnt].next=rhead[f];
	redge[cnt].to=t;
	rhead[f]=rcnt;
}
int vis[N];
inline void spfa(){
	std::queue<int> q;
	for(int i=1;i<=n;i++)
		q.push(i),vis[i]=1;
	while(!q.empty()){
		int u=q.front();q.pop(),vis[u]=0;
		ll tmp=a[u];
		for(int i=head[u];i;i=edge[i].next){
			int v=edge[i].to;
			tmp+=dis[v];
		}
		if(tmp<dis[u]){
			dis[u]=tmp;
			for(int i=rhead[u];i;i=redge[i].next){
				int v=redge[i].to;
				if(!vis[v])vis[v]=1,q.push(v);
			}
		}
	}
}
int main(){
	n=rd();
	for(int i=1;i<=n;i++){
		a[i]=rd(),dis[i]=rd();int k=rd(),x;
		while(k--){
			x=rd(),add(i,x),radd(x,i);
		}
	}
	spfa();
	printf("%lld
",dis[1]);
	return 0;
}

[AHOI2014/JSOI2014]奇怪的计算器

题目链接

显然是把所有数读入后对整个序列进行操作后一起输出。本题涉及 (5) 种操作:区间加(减法被包括在内),区间乘,区间加 (a_i*X),区间对 (L)(max) 和区间对 (R)(min)。后两种操作可以转化为将原序列排序,易知序列的单调性是不变的,用线段树维护最大最小值,将应该处理的区间进行区间赋值。

当然可以对于每一个操作维护懒标记在线段树上直接搞。但还是有更加简便的一种处理方式:定义一种操作为 (f(k1,k2,k3)),需要更新数值时把线段树上的数值 (val_i) 变成 (val_i*k1+a_i*k2+k3),就可以用它支持所有的操作了。

  1. 区间加 (c) : (f(1,0,c))

  2. 区间乘 (c) : (f(c,0,0))

  3. 区间加 (a_i*c) : (f(1,c,0))

  4. 区间赋值为 (c) : (f(0,0,c))

于是维护 (3) 个标记,统一下传。

#include <cstdio>
#include <algorithm>
#define int ll
typedef long long ll;
inline int rd(){
	int x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
const int N=100002;
int q,n,L,R;
int op[N],x[N];
struct node{
	int val,id;
	bool operator < (const node &y)const{
		return val<y.val;
	}
}a[N];
ll tmax[N<<2],tmin[N<<2];
ll k1[N<<2],k2[N<<2],k3[N<<2];
int ans[N];
inline void pushup(int rt){
	tmin[rt]=tmin[rt<<1];
	tmax[rt]=tmax[rt<<1|1];
}
inline void pushtag(int l,int r,int rt,int x,int y,int z){//tree[rt]=tree[rt]*k1+val*k2+k3;
	k1[rt]*=x,k2[rt]=k2[rt]*x+y,k3[rt]=k3[rt]*x+z;
	tmin[rt]=tmin[rt]*x+a[l].val*y+z;
	tmax[rt]=tmax[rt]*x+a[r].val*y+z;
}
inline void pushdown(int rt,int l,int r){
	int mid=(l+r)>>1;
	pushtag(l,mid,rt<<1,k1[rt],k2[rt],k3[rt]);
	pushtag(mid+1,r,rt<<1|1,k1[rt],k2[rt],k3[rt]);
	k1[rt]=1,k2[rt]=k3[rt]=0;
}
inline void build(int l,int r,int rt){
	k1[rt]=1,k2[rt]=k3[rt]=0;
	if(l==r){
		tmin[rt]=tmax[rt]=a[l].val;
		return;
	}
	int mid=(l+r)>>1;
	build(l,mid,rt<<1);
	build(mid+1,r,rt<<1|1);
	pushup(rt);
}
inline void update(int l,int r,int rt,int op){
	if(l==r){
		if(!op)pushtag(l,r,rt,0,0,L);
		else pushtag(l,r,rt,0,0,R);
		return;
	}
	int mid=(l+r)>>1;
	pushdown(rt,l,r);
	if(!op){
		if(tmin[rt<<1|1]<L)pushtag(l,mid,rt<<1,0,0,L),update(mid+1,r,rt<<1|1,op);
		else update(l,mid,rt<<1,op);
	}
	else{
		if(tmax[rt<<1]>R)pushtag(mid+1,r,rt<<1|1,0,0,R),update(l,mid,rt<<1,op);
		else update(mid+1,r,rt<<1|1,op);
	}
	pushup(rt);
}
inline void dfs(int l,int r,int rt){
	if(l==r){
		ans[a[l].id]=tmin[rt];
		return;
	}
	int mid=(l+r)>>1;
	pushdown(rt,l,r);
	dfs(l,mid,rt<<1);
	dfs(mid+1,r,rt<<1|1);
	pushup(rt);
}
signed main(){
	q=rd(),L=rd(),R=rd();
	for(int i=1;i<=q;i++){
		char s[3];scanf("%s",s);
		if(s[0]=='+')op[i]=1;
		else if(s[0]=='-')op[i]=2;
		else if(s[0]=='*')op[i]=3;
		else op[i]=4;
		x[i]=rd();
	}
	n=rd();
	for(int i=1;i<=n;i++)a[i].val=rd(),a[i].id=i;
	std::sort(a+1,a+n+1);
	build(1,n,1);
	for(int i=1;i<=q;i++){
		if(op[i]==1)pushtag(1,n,1,1,0,x[i]);
		else if(op[i]==2)pushtag(1,n,1,1,0,-x[i]);
		else if(op[i]==3)pushtag(1,n,1,x[i],0,0);
		else pushtag(1,n,1,1,x[i],0);
		if(tmin[1]<L)update(1,n,1,0);
		if(tmax[1]>R)update(1,n,1,1);
	}
	dfs(1,n,1);
	for(int i=1;i<=n;i++)printf("%lld
",ans[i]);
	return 0;
}

[JSOI2014]支线剧情2

题目链接

树形 (dp)。设 (s_i) 为以 (i) 为根的子树的叶子个数,(f_i) 为不使用存档走完以 (i) 为根的子树需要的时间,则 (f_i=sumlimits_{jin son_i}f_j+s_j*dis(i,j))

再设 (g_i) 为可以进行存档时走完子树 (i) 需要的最小时间。初值 (g_i=f_i),枚举每一个子节点令它为存档点进行转移。

#include <cstdio>
typedef long long ll;
inline int rd(){
	int x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
inline ll min(ll x,ll y){return x<y?x:y;}
const int N=1000002;
int n;
struct Edge{
	int to,next;
	ll w;
}edge[N<<1];
int head[N],cnt;
int size[N];
ll f[N],g[N];
inline void add(int f,int t,ll w){
	edge[++cnt].next=head[f];
	edge[cnt].to=t;
	edge[cnt].w=w;
	head[f]=cnt;
}
inline void dp(int u,ll dis){
	if(!head[u]){
		size[u]=1;
		return;
	}
	ll sum=0;
	for(int i=head[u];i;i=edge[i].next){
		int v=edge[i].to;ll w=edge[i].w;
		dp(v,dis+w);
		size[u]+=size[v],f[u]+=f[v]+w*size[v];
		sum+=min(g[v]+dis+w,f[v]+w*size[v]);
	}
	g[u]=f[u];
	for(int i=head[u];i;i=edge[i].next){
		int v=edge[i].to;ll w=edge[i].w;
		g[u]=min(g[u],sum-min(g[v]+dis+w,f[v]+w*size[v])+g[v]+w);
	}
}
int main(){
	n=rd();
	for(int i=1;i<=n;i++){
		int x=rd(),u,w;
		while(x--){
			u=rd(),w=rd();
			add(i,u,w);
		}
	}
	dp(1,0);
	printf("%lld
",g[1]);
	return 0;
}

[JSOI2014]强连通图

题目链接

第一问就是最大强连通分量的大小。第二问的话先缩点,然后可以手玩一下几组样例,发现就是缩点之后入度、出度为 (0) 的点数之间的最大值。具体的证明可以在这篇博客查看。

#include <cstdio>
typedef long long ll;
inline int rd(){
	int x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
inline int min(int x,int y){return x<y?x:y;}
inline int max(int x,int y){return x>y?x:y;}
const int N=1000002;
int n,m;
struct Edge{
	int to,next;
}edge[N];
int head[N],cnt;
int u[N],v[N];
int dfn[N],low[N],col[N],st[N],size[N],c,top,time;
int in[N],out[N],cnti,cnto;
int ans;
inline void add(int f,int t){
	edge[++cnt].next=head[f];
	edge[cnt].to=t;
	head[f]=cnt;
}
inline void dfs(int u){
	dfn[u]=low[u]=++time;
	st[++top]=u;
	for(int i=head[u];i;i=edge[i].next){
		int v=edge[i].to;
		if(!dfn[v]){
			dfs(v);
			low[u]=min(low[u],low[v]);
		}
		else if(!col[v])low[u]=min(low[u],dfn[v]);
	}
	if(dfn[u]==low[u]){
		col[u]=++c,size[c]=1;
		while(st[top]!=u){
			col[st[top--]]=c;
			size[c]++;
		}
		top--;
	}
}
int main(){
	n=rd(),m=rd();
	for(int i=1;i<=m;i++){
		u[i]=rd(),v[i]=rd();
		add(u[i],v[i]);
	}
	for(int i=1;i<=n;i++)if(!dfn[i])dfs(i);
	for(int i=1;i<=c;i++)ans=max(ans,size[i]);
	printf("%d
",ans);
	for(int i=1;i<=m;i++){
		if(col[u[i]]!=col[v[i]])
			in[col[v[i]]]++,out[col[u[i]]]++;
	}
	for(int i=1;i<=c;i++)cnti+=(!in[i]),cnto+=(!out[i]);
	printf("%d
",max(cnti,cnto));
	return 0;
}

[JSOI2014]歌剧表演

题目链接

考虑维护每一个演员出演的场次,如果他出演的场次唯一就可以被辨认出。用集合维护演员,在一个集合内的演员出演场次一样,即无法辨认。对于每一次出演,将原来处于同一集合中的本次出演过的演员弄出来,组成一个新的集合。若新集合元素唯一或是原集合元素唯一则可以确定对应演员的身份。

#include <cstdio>
#include <algorithm>
#include <set>
typedef long long ll;
inline int rd(){
	int x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
const int N=100002;
std::set<int> s[N];
int n,q,a[N];
int c=1;
int id[N],ans[N];
bool comp(const int &x,const int &y){
	return id[x]<id[y];
}
int main(){
	n=rd(),q=rd();
	for(int i=1;i<=n;i++){
		id[i]=1;
		s[1].insert(i);
	}
	for(int m=1;m<=q;m++){
		int k=rd();
		for(int i=1;i<=k;i++)a[i]=rd();
		std::sort(a+1,a+k+1,comp);
		a[k+1]=0;
		for(int i=1;i<=k;i++){
			int now=id[a[i]],l=i,r=i;
			while(id[a[r+1]]==now)r++;
			if(s[now].size()!=r-l+1){
				c++;
				for(int j=l;j<=r;j++)
					id[a[j]]=c,s[now].erase(a[j]),s[c].insert(a[j]);
				if(s[now].size()==1&&!ans[*s[now].begin()])ans[*s[now].begin()]=m;
				if(s[c].size()==1&&!ans[*s[c].begin()])ans[*s[c].begin()]=m;
			}
			i=r;
		}
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," 
"[i==n]);
	return 0;
}

[JSOI2014]序列维护

题目链接

裸的线段树维护区间。直接维护加、乘两个标记即可。

#include <cstdio>
typedef long long ll;
inline int rd(){
	int x=0,p=1;
	char a=getchar();
	while((a<48||a>57)&&a!='-')a=getchar();
	if(a=='-')p=-p,a=getchar();
	while(a>47&&a<58)x=(x<<1)+(x<<3)+(a&15),a=getchar();
	return x*p;
}
const int N=100002;
int n,p,q,a[N];
ll tree[N<<2],add[N<<2],mul[N<<2];
inline void pushup(int rt){
	tree[rt]=(tree[rt<<1]+tree[rt<<1|1])%p;
}
inline void pushdown(int rt,int ls,int rs){
	if(add[rt]||mul[rt]!=1){
		mul[rt<<1]=mul[rt]*mul[rt<<1]%p;
		mul[rt<<1|1]=mul[rt]*mul[rt<<1|1]%p;
		add[rt<<1]=add[rt<<1]*mul[rt]%p;
		add[rt<<1|1]=add[rt<<1|1]*mul[rt]%p;
		tree[rt<<1]=tree[rt<<1]*mul[rt]%p;
		tree[rt<<1|1]=tree[rt<<1|1]*mul[rt]%p;
		add[rt<<1]=(add[rt]+add[rt<<1])%p;
		add[rt<<1|1]=(add[rt]+add[rt<<1|1])%p;
		tree[rt<<1]=(tree[rt<<1]+add[rt]*ls)%p;
		tree[rt<<1|1]=(tree[rt<<1|1]+add[rt]*rs)%p;
		add[rt]=0,mul[rt]=1;
	}
}
inline void build(int l,int r,int rt) {
	add[rt]=0,mul[rt]=1;
	if(l==r){
		tree[rt]=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(l,mid,rt<<1);
	build(mid+1,r,rt<<1|1);
	pushup(rt);
}
inline void update1(int l,int r,int rt,int L,int R,int k){
	if(L<=l&&r<=R){
		tree[rt]=(tree[rt]+k*(r-l+1))%p;
		add[rt]=(add[rt]+k)%p;
		return;
	}
	int mid=(l+r)>>1;
	pushdown(rt,mid-l+1,r-mid);
	if(L<=mid)update1(l,mid,rt<<1,L,R,k);
	if(R>mid)update1(mid+1,r,rt<<1|1,L,R,k);
	pushup(rt);
}
inline void update2(int l,int r,int rt,int L,int R,int k){
	if(L<=l&&r<=R){
		mul[rt]=mul[rt]*k%p;
		add[rt]=add[rt]*k%p;
		tree[rt]=tree[rt]*k%p;
		return;
	}
	int mid=(l+r)>>1;
	pushdown(rt,mid-l+1,r-mid);
	if(L<=mid)update2(l,mid,rt<<1,L,R,k);
	if(R>mid)update2(mid+1,r,rt<<1|1,L,R,k);
	pushup(rt);
}
inline ll query(int l,int r,int rt,int L,int R){
	if(L<=l&&r<=R)return tree[rt];
	int mid=(l+r)>>1;
	ll ans=0;
	pushdown(rt,mid-l+1,r-mid);
	if(L<=mid)ans=query(l,mid,rt<<1,L,R);
	if(R>mid)ans=(ans+query(mid+1,r,rt<<1|1,L,R))%p;
	return ans;
}
int main(){
	n=rd(),p=rd();
	for(int i=1;i<=n;i++)a[i]=rd()%p;
	build(1,n,1);
	q=rd();
	while(q--){
		int op,x,y,k;
		op=rd(),x=rd(),y=rd();
		if(op==1){
			k=rd();
			update2(1,n,1,x,y,k);
		}
		else if(op==2){
			k=rd();
			update1(1,n,1,x,y,k);
		}
		else printf("%lld
",query(1,n,1,x,y));
	}
	return 0;
}

小结

( ext{JSOI2014}) 的题目以 (dp)、贪心、图论、数据结构的运用为主,也存在一些比较少见的算法如三分,思维难度也较大,目前仍有几道题未完成,部分涉及网络流、( ext{2-SAT}) 等尚未自学算法,这些算法还是需要自己去落实;剩余的大多数为思维题,要求自己有时间自己思考并完成。

原文地址:https://www.cnblogs.com/wsk1202/p/12264660.html