JSOI2010/2011 总结

[JSOI2010]旅行

题目链接

挺神的一道 \(dp\) 题。枚举 \(t\),代表选择排序后前 \(t\) 条边在从 \(1\)\(n\) 新的路径上,设 \(f_{i,j,k}\) 表示到点 \(i\),经过前 \(t\) 条中的 \(j\) 条边,使用了 \(k\) 次交换边权的最短路,用最短路的形式进行 \(dp\)。转移有:

\(f_{to,j+1,k}=f_{u,j,k}+w_{j+1}\),前 \(t\) 条边包括当前边。

\(f_{to,j+1,k+1}=f_{u,j,k}+w_{j+1}\),前 \(t\) 条边不包括当前边并使用一次交换。

\(f_{to,j,k}=f_{u,j,k}+w\),前 \(t\) 条边不包括当前边,不使用交换(此处 \(w\) 指当前边的边权)。

时间复杂度大概是\(O(nm^2k)\),反正可以过。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
#define inf (2100000000)
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;}
const int N=52,S=152;
int n,m,s;
struct Edge{
	int u,v,w;
}e[S];
bool comp(const Edge &x,const Edge &y){
	return x.w<y.w;
}
struct node{
	int i,j,k;
	node(int u=0,int e=0,int p=0){i=u,j=e,k=p;}
};
std::vector<int> p[N];
int vis[N][S][N],f[N][S][N];
int ans=inf;
inline void bfs(int t){
	std::queue<node> q;
	memset(vis,0,sizeof vis),memset(f,0x3f,sizeof f);
	f[1][0][0]=0,q.push(node(1,0,0)); 
	while(!q.empty()){
		node now=q.front();q.pop();
		int u=now.i,j=now.j,k=now.k;vis[u][j][k]=0;
		for(int i=0,sz=p[u].size();i<sz;i++){
			int id=p[u][i],v=e[id].u;
			if(v==u)v=e[id].v;
			if(id<=t){
				if(j<t&&f[u][j][k]+e[j+1].w<f[v][j+1][k]){
					f[v][j+1][k]=f[u][j][k]+e[j+1].w;
					if(!vis[v][j+1][k])vis[v][j+1][k]=1,q.push(node(v,j+1,k));
				}
			}
			else{
				if(j<t&&k<s&&f[u][j][k]+e[j+1].w<f[v][j+1][k+1]){
					f[v][j+1][k+1]=f[u][j][k]+e[j+1].w;
					if(!vis[v][j+1][k+1])vis[v][j+1][k+1],q.push(node(v,j+1,k+1));
				}
				if(f[u][j][k]+e[id].w<f[v][j][k]){
					f[v][j][k]=f[u][j][k]+e[id].w;
					if(!vis[v][j][k])vis[v][j][k]=1,q.push(node(v,j,k));
				}
			}
		}
	}
}
int main(){
	n=rd(),m=rd(),s=rd();
	for(int i=1;i<=m;i++)e[i].u=rd(),e[i].v=rd(),e[i].w=rd();
	std::sort(e+1,e+m+1,comp);
	for(int i=1;i<=m;i++)
		p[e[i].u].push_back(i),p[e[i].v].push_back(i);
	for(int i=0;i<=m;i++){
		bfs(i);
		for(int j=0;j<=s;j++)ans=min(ans,f[n][i][j]);
	}
	printf("%d\n",ans);
	return 0;
}

[JSOI2010]找零钱的洁癖

题目链接

正解不知道。

由于种类数较少可以先暴力 \(bfs\),如果快爆了还没有搜到答案就及时结束,用贪心算出一个答案。然后就过了。

求正解qwq

#include <cstdio>
#include <map>
#define inf (210000000000ll)
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=1000002;
std::map<ll,int> mp;
int n;
ll x,a[N];
ll v[N],s[N];
int hd=0,tl=1;
inline ll bfs(){
	while(hd<tl){
		hd++;
		for(int i=1;i<=n;i++){
			tl++;
			if(tl==N-2)return inf;
			s[tl]=s[hd]+1;
			if(v[hd]<x)v[tl]=v[hd]+a[i];
			else v[tl]=v[hd]-a[i];
			if(v[tl]==x)return s[tl];
			if(mp[v[tl]])tl--;
			else mp[v[tl]]=1;
		}
	}
	return inf;
}
inline ll get(ll x){
	ll ans=0;
	for(int i=n;i>=1;i--){
		if(!a[i])continue;
		ans+=x/a[i];
		x-=x/a[i]*a[i];
	}
	if(x)return inf;
	return ans;
}
int main(){
	x=rd();
	int i=1;ll k;
	while(~scanf("%lld",&k))a[i++]=k;
	n=i-1;
	printf("%lld\n",min(bfs(),get(x)));
	return 0;
}

[JSOI2010]挖宝藏

题目链接

对于一个宝藏,如果要挖到它,则必须挖掉它上面的一个倒三角形,这个三角形会对应 \(x\) 轴的一段区间,这是可以直接算出来的。所以我们把每一个宝藏存在一个结构体里,包含这个区间、它本身的价值、挖到它至少得到的价值(即把上面的三角形中所有的宝藏的价值加起来)和挖它的代价。

考虑 \(dp\)。设 \(f_i\) 表示挖前 \(i\) 个宝藏,第 \(i\) 个必须挖能得到的最大价值。

先将宝藏按右端点从小到大排序,枚举 \(1\le j<i\),如果 \(j\) 宝藏的区间与 \(i\) 的区间无交集,则可以直接取 \(f_j\) 进行转移。

\(i\)\(j\) 有交,则维护一个指针,暴力处理重复的部分。由于宝藏的右端点是单调的,所以指针只会右移,复杂度仍然是 \(O(n)\)

总复杂度是 \(O(n^2)\),足以通过本题。

#include <cstdio>
#include <algorithm>
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 max(int x,int y){return x>y?x:y;}
const int N=1002;
int n;
struct node{
	int l,r,p1,p2,c;
	bool operator < (const node &y)const{
		return (r^y.r)?(r<y.r):(l<y.l);
	}
}a[N];
int f[N],ans;
inline int calc(int l,int r){
	return (r-l+2)*(r-l+2)/4;
}
int main(){
	n=rd();
	for(int i=1;i<=n;i++){
		int x=rd(),y=rd(),p=rd();
		a[i].l=x+y+1,a[i].r=x-y-1,a[i].p1=p,a[i].p2=0,a[i].c=calc(x+y+1,x-y-1);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(a[i].l<=a[j].l&&a[i].r>=a[j].r)a[i].p2+=a[j].p1;
	std::sort(a+1,a+n+1);
	for(int i=1;i<=n;i++){
		f[i]=a[i].p2-a[i].c;
		int now=1,sum=0;
		for(int j=1;j<i;j++){
			if(a[j].l<a[i].l&&a[j].r>=a[i].l){
				while(now<=i&&a[now].r<=a[j].r){
					if(a[now].l>=a[i].l)sum+=a[now].p1;
					now++;
				}
				f[i]=max(f[i],f[j]+a[i].p2-a[i].c-(sum-calc(a[i].l,a[j].r)));
			}
			if(a[j].r<a[i].l)f[i]=max(f[i],f[j]+a[i].p2-a[i].c);
		}
	}
	for(int i=0;i<=n;i++)ans=max(ans,f[i]);
	printf("%d\n",ans);
	return 0;
}

[JSOI2010]排名

题目链接

对于第一问,从 \(i\) 向非零的 \(a_i\) 连边,用一个大根堆去跑拓扑排序,把先遇到的点的排名往后排。

对于第二问,从非零的 \(a_i\)\(i\) 连边,遇到 \(a_i=0\)\(i\) 最开始就可以扔进大根堆,然后直接遍历,把先遇到的点的排名往前排。

#include <cstdio>
#include <queue>
#include <cstring>
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=200002;
struct Edge{
	int to,next;
}edge[N<<1];
int n,a[N];
int head[N],cnt,in[N];
int ans[N],now;
inline void add(int f,int t){
	edge[++cnt].next=head[f];
	edge[cnt].to=t;
	head[f]=cnt;
}
inline void init(){
	memset(head,0,sizeof head);
}
inline void work1(){
	std::priority_queue<int> q;
	init();
	for(int i=1;i<=n;i++){
		if(!a[i])continue;
		add(i,a[i]),in[a[i]]++;
	}
	for(int i=1;i<=n;i++)
		if(!in[i])q.push(i);
	now=n;
	while(!q.empty()){
		int u=q.top();q.pop();
		ans[u]=now--;
		for(int i=head[u];i;i=edge[i].next){
			int v=edge[i].to;
			if(!--in[v])q.push(v);
		}
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
}
inline void work2(){
	std::priority_queue<int> q;
	init();
	for(int i=1;i<=n;i++){
		if(!a[i])q.push(i);
		else add(a[i],i);
	}
	now=1;
	while(!q.empty()){
		int u=q.top();q.pop();
		ans[u]=now++;
		for(int i=head[u];i;i=edge[i].next){
			int v=edge[i].to;
			q.push(v);
		}
	}
	for(int i=1;i<=n;i++)printf("%d%c",ans[i]," \n"[i==n]);
}
int main(){
	n=rd();
	for(int i=1;i<=n;i++)a[i]=rd();
	work1();
	work2();
	return 0;
}

[JSOI2011]柠檬

题目链接

斜率优化 \(dp\)。显然每一个小段首尾的 \(s_i\) 要相同,并且 \(s_0\) 也应选择这种 \(s_i\)。记录 \(sum_i\) 表示大小为 \(s_i\) 的贝壳在 \(1-i\) 中出现的次数,设 \(f_i\) 代表把前 \(i\) 个数分段得到的最大柠檬数,可以得到以下转移方程:

\[f_i=\max_{1\le j\le i, s_i=s_j} \{f_{j-1}+s_i*(sum_i-sum_j+1)^2\} \]

把式子拆开,得到:

\[f_{j-1}+s_j*sum_i^2=2*s_j*sum_j*(sum_i+1)+f_i-s_i*(sum_i+1)^2 \]

\(x=2*s_j*sum_j\)\(y=f_{j-1}+s_j*sum_i^2\),使用单调栈维护上凸包。实现可以使用 \(vector\) 以节约空间。

注意代码中的变量名和题解中的不太一样。

#include <cstdio>
#include <vector>
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 max(ll x,ll y){return x>y?x:y;}
const int N=100002,C=10002;
int n;
ll a[N],lst[N],s[N];
ll f[N];
std::vector <int> q[C];
inline ll sqr(ll x){return x*x;}
inline ll X(int i){return 2*a[i]*s[i];}
inline ll Y(int i){return f[i-1]+a[i]*sqr(s[i]);}
inline double slope(int i,int j){return 1.0*(Y(i)-Y(j))/(X(i)-X(j));}
#define top q[now].size()-1
int main(){
	n=rd();
	for(int i=1;i<=n;i++)a[i]=rd(),s[i]=s[lst[a[i]]]+1,lst[a[i]]=i;
	for(int i=1;i<=n;i++){
		int now=a[i];
		while(q[now].size()>1&&slope(q[now][top],q[now][top-1])<slope(q[now][top],i))q[now].pop_back();
		q[now].push_back(i);
		while(q[now].size()>1&&slope(q[now][top],q[now][top-1])<s[i]+1)q[now].pop_back();
		f[i]=f[q[now].back()-1]+a[i]*sqr(s[i]-s[q[now].back()]+1);
	}
	printf("%lld\n",f[n]);
	return 0;
}

[JSOI2011]分特产

题目链接

组合+容斥。若没有每一个人必须有的限制,答案为 \(\prod\limits_{i=1}^m\dbinom{a_i+n-1}{n-1}\)。有了限制的话考虑容斥,枚举至少有 \(i\) 个没有,剩下的可以随便给。所以答案为:

\[ans=\sum\limits_{i=0}^n(-1)^i\dbinom{n}{i}\prod\limits_{j=1}^m\dbinom{n-i-1+a_j}{n-i-1} \]

#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=5002,S=3000;
const ll mod=1e9+7;
ll fac[N],inv[N];
int n,m;
ll a[N],ans;
inline ll fpow(ll b,ll p=mod-2){
	ll ans=1,tmp=b;
	while(p){
		if(p&1)ans=ans*tmp%mod;
		tmp=tmp*tmp%mod;
		p>>=1;
	}
	return ans;
}
inline ll C(int n,int m){
	return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
int main(){
	fac[0]=1;
	for(int i=1;i<=S;i++)fac[i]=fac[i-1]*i%mod;
	inv[S]=fpow(fac[S]);
	for(int i=S-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
	n=rd(),m=rd();
	for(int i=1;i<=m;i++)a[i]=rd();
	for(int i=0,p=0;i<=n;i++,p^=1){
		ll t=C(n,i);
		if(p)t=mod-t;
		for(int j=1;j<=m;j++)t=t*C(n-i-1+a[j],n-i-1)%mod;
		ans=(ans+t)%mod;
	}
	printf("%lld\n",ans);
    return 0;
}

[JSOI2011]棒棒糖

题目链接

建一棵主席树,查询的时候对于每个节点判断一下,如果这个区间内 \(\le mid\) 的数的个数满足条件就往左走,如果 \(>mid\) 的数的个数满足就往右走。都没有就是 \(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;
}
const int N=50002;
int n,q,a[N],Size;
int rt[N];
int son[N<<5][2],val[N<<5];
#define ls son[rt][0]
#define rs son[rt][1]
#define vl son[vs][0]
#define vr son[vs][1]
inline void update(int l,int r,int &rt,int vs,int x){
    rt=++Size;
    ls=vl,rs=vr,val[rt]=val[vs]+1;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)update(l,mid,ls,vl,x);
    else update(mid+1,r,rs,vr,x);
}
inline int query(int l,int r,int rt,int vs,int k){
    if(l==r)return l;
    int mid=(l+r)>>1;
    if(val[vl]-val[ls]>k)return query(l,mid,ls,vl,k);
    else if(val[vr]-val[rs]>k)return query(mid+1,r,rs,vr,k);
    else return 0;
}
#undef ls
#undef rs
#undef vl
#undef vr
int main(){
    n=rd(),q=rd();
    for(int i=1;i<=n;i++)a[i]=rd(),update(1,n,rt[i],rt[i-1],a[i]);
    while(q--){
        int l=rd(),r=rd();
        printf("%d\n",query(1,n,rt[l-1],rt[r],(r-l+1)>>1));
    }
    return 0;
}

[JSOI2011]任务调度

题目链接

虽然是挺裸的可并堆,但还是不想手写左偏树。。所以用 \(pb\_ds\) 水过去了,跑的还挺快?

#include <cstdio>
#include <algorithm>
#include <map>
#include<ext/pb_ds/priority_queue.hpp>
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=300002,S=502;
int n,m,t;
int val[N];
struct node{
	int val,id;
	node(int i=0,int v=0){val=v,id=i;}
};
struct cmp{
	bool operator () (node x,node y){
		return x.val>y.val;
	}
};
__gnu_pbds::priority_queue<node,cmp> q[S];
__gnu_pbds::priority_queue<node,cmp>::point_iterator it[N];
int main(){
	n=rd(),m=rd(),t=rd();
	while(t--){
		char s[10];int x,k,w;
		scanf("%s",s);
		if(s[0]=='A'){
			x=rd(),k=rd(),w=rd();
			val[k]=w;
			it[k]=q[x].push(node(k,w));
		}
		if(s[0]=='D'){
			x=rd(),k=rd(),w=rd();
			val[k]-=w;
			q[x].modify(it[k],node(k,val[k]));
		}
		if(s[0]=='T'){
			x=rd(),k=rd();
			q[k].join(q[x]);
		}
		if(s[0]=='M'){
			x=rd();
			printf("%d\n",q[x].top().val);
		}
		if(s[0]=='W'){
			x=rd(),w=rd();
			node tmp=q[x].top();q[x].pop();
			if((!q[x].empty())&&q[x].top().val==tmp.val){
				puts("ERROR");
				it[tmp.id]=q[x].push(tmp);
			}
			else{
				k=tmp.id,val[k]+=w;
				it[k]=q[x].push(node(k,val[k]));
			}
		}
	}
	return 0;
}

小结

\(\text{JSOI2010/2011}\) 的题目主要考察了 \(dp\) 及其优化、图论,数论和数据结构也有所涉及。其中俄罗斯方块、游戏两题暂时不会,找零钱那题也是通过分析数据的特殊性写了假算法,正解争取以后补上。通过这两年的做题,我发现自己的思维仍然不足以完成省选难度的部分题目,而且有些算法还是掌握的不太熟练,应该多做题以掌握足够的算法、技巧等。

posted @ 2020-02-03 14:31  wsk1202  阅读(178)  评论(0编辑  收藏  举报