NOIP2017模拟赛总结(2017.10.23-2017.10.25)

在学校模拟赛中出现了一些比较有意思的题目,在这里写出来总结一下。一篇博客就写3天,也就是9题吧。

2017.10.23 Problem A
题目大意: 有一列数: 1 1 1~ n n n,每次从其中选出两个求平均值,然后放回数列中,求最后剩下的数的最大值,对大素数取模。
做法: 本题需要用到贪心+数学。
首先可以证明每次选最小的两个是最优的,那么最后得到的值要怎么计算呢?
我们知道要求的式子是: ( ( . . . ( ( ( 1 + 2 ) × 1 2 + 3 ) × 1 2 + 4 ) . . . ) × 1 2 + n ) × 1 2 ((...(((1+2)\times \frac{1}{2}+3)\times \frac{1}{2}+4)...)\times \frac{1}{2}+n)\times \frac{1}{2} ((...(((1+2)×21+3)×21+4)...)×21+n)×21,那么:
原 式 = 1 2 n − 1 + ∑ i = 2 n i 2 n − i + 1 = 1 2 n − 1 + ∑ i = 2 n i 2 i − 2 2 n − 1 = 2 n − 1 + ∑ i = 0 n − 2 ( 2 n − 1 − 2 i ) 2 n − 1 = n − 1 + 1 2 n − 1 原式=\frac{1}{2^{n-1}}+\sum_{i=2}^n \frac{i}{2^{n-i+1}}=\frac{1}{2^{n-1}}+\sum_{i=2}^n\frac{i2^{i-2}}{2^{n-1}}=\frac{2^{n-1}+\sum_{i=0}^{n-2}(2^{n-1}-2^i)}{2^{n-1}}=n-1+\frac{1}{2^{n-1}} =2n11+i=2n2ni+1i=2n11+i=2n2n1i2i2=2n12n1+i=0n2(2n12i)=n1+2n11
至于倒数第二个式子是怎么推出来的,可以手画个矩阵找规律,第一列写 1 1 1 2 0 2^0 20,第二列写 2 2 2 2 0 2^0 20,第三列写 3 3 3 2 1 2^1 21,…,第 n n n列写 n n n 2 n − 2 2^{n-2} 2n2,我们发现我们要求的就是这一堆东西的和除以 2 n − 1 2^{n-1} 2n1,然后我们知道每一行和的和等于每一列和的和,就可以推出上面的式子了。然后用快速幂和逆元处理一下即可。时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mod 1000000007
using namespace std;
ll n;

ll power(ll a,ll b)
{
	ll s=1,ss=a;
	while(b)
	{
		if (b&1) s=(s*ss)%mod;
		b>>=1,ss=(ss*ss)%mod;
	}
	return s;
}

int main()
{
	scanf("%lld",&n);
	if (n==0) printf("0");
	else printf("%lld",(n-1+power(power(2,n-1),mod-2))%mod);
	
	return 0;
}

2017.10.23 Problem B
题目大意: 给出一个带边权的无向连通图,令 d i s ( i , j ) dis(i,j) dis(i,j)为点 i , j i,j i,j之间路径上边权最小值的最大值,所有无序点对 ( i , j ) (i,j) (i,j) d i s ( i , j ) dis(i,j) dis(i,j)中第 k k k大的值。
做法: 本题需要用到Kruskal算法求最大生成树。
分析题目,我们发现求个最大生成树后,两点之间路径上边权的最小值就是它们的 d i s dis dis。那么考虑怎么快速求和,我们发现,因为求最大生成树时,边是按边权从大到小的顺序插入的,那么每当一条边连通两个连通块时,对于两点处于不同连通块的点对,它们之间的 d i s dis dis就是当前所添加的边的边权,那么我们用并查集记录集合中的点数,每次合并时,就有两个集合中点数之积个无序点对的 d i s dis dis为当前边权,因为边权按从大到小顺序处理,就这样找第 k k k大即可。时间复杂度 O ( m log ⁡ m ) O(m\log m) O(mlogm)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
int n,m,ed[100010],fa[100010],tot;
ll k,siz[100010],num[100010];
struct edge {int u,v,w;} e[200010];

int find(int x)
{
	int r=x,i=x,j;
	while (r!=fa[r]) r=fa[r];
	while (i!=r) j=fa[i],fa[i]=r,i=j;
	return r;
}

void merge(int x,int y,int w)
{
	int fx=find(x),fy=find(y);
	ed[++tot]=w;
	num[tot]=siz[fx]*siz[fy];
	fa[fx]=fy,siz[fy]+=siz[fx];
}

bool cmp(edge a,edge b)
{
	return a.w>b.w;
}

void kruskal()
{
	sort(e+1,e+m+1,cmp);
	for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;
	tot=0;
	for(int i=1;i<=m;i++)
	{
		if (tot>=n-1) break;
		if (find(e[i].u)!=find(e[i].v)) merge(e[i].u,e[i].v,e[i].w);
	}
}

int main()
{
	scanf("%d%d%lld",&n,&m,&k);
	for(int i=1;i<=m;i++)
		scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
	kruskal();
	for(int i=1;i<=tot;i++)
	{
		if (k<=num[i]) {printf("%d",ed[i]);break;}
		else k-=num[i];
	}
	
	return 0;
}

2017.10.23 Problem C
题目大意: 给定一个由小写字母组成的字符串,每个位置有一个权值 d i f dif dif,要将字符串分为若干段连续的区间,一段连续段 [ l , r ] [l,r] [l,r]的代价取下列情况的最小值:
情况1:代价为 a × ( ∑ i = l r d i f ( i ) ) 2 + b a\times (\sum_{i=l}^rdif(i))^2+b a×(i=lrdif(i))2+b
情况2:若该段内出现次数最多的字符出现的次数在 [ L , R ] [L,R] [L,R]之间,代价为 c × ∑ i = l r d i f ( i ) + d c\times\sum_{i=l}^rdif(i)+d c×i=lrdif(i)+d
求字符串每一个前缀划分后的最小代价。
做法: 本题需要用到斜率+单调队列优化DP。
其实挺容易看出来情况1就是一个标准斜率优化DP的式子,那么第二种情况是什么呢?观察后得出,当 r r r变大时,满足情况2的 l l l的区间也是单调向右移动的,用单调队列维护这一种情况,然后取上述两种情况的最小值即可。
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define LL long long
using namespace std;
int n,l,r,ll[100010],rr[100010],now[30]={0},q[100010],qq[100010],h,t,hh,tt,nowr;
LL a,b,c,d,inf,dif[100010],sum[100010],f[100010];
char s[100010];
struct point
{
	LL x,y;
	point operator - (point a) const
	{
		point now;
		now.x=x-a.x;
		now.y=y-a.y;
		return now;
	}
} p[100010],check;

int maxx()
{
	int mx=0;
	for(int i=0;i<26;i++) mx=max(mx,now[i]);
	return mx;
}

LL multi(point a,point b)
{
	return a.x*b.y-b.x*a.y;
}

int main()
{
	inf=1000000000;
	inf*=inf;
	
	scanf("%d%lld%lld%lld%lld%d%d",&n,&a,&b,&c,&d,&l,&r);
	scanf("%s",s+1);
	sum[0]=0;
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&dif[i]);
		sum[i]=sum[i-1]+dif[i];
	}
	
	int x=1;
	for(int i=1;i<=n;i++)
	{
		now[s[i]-'a']++;
		while (maxx()>r) now[s[x]-'a']--,x++;
		ll[i]=x;
	}
	memset(now,0,sizeof(now));
	x=n+1;
	for(int i=n;i>=1;i--)
	{
		if (i<n) now[s[i+1]-'a']--;
		while (maxx()<l&&x>1) now[s[--x]-'a']++;
		if (maxx()<l) rr[i]=0;
		else rr[i]=x;
	}
	
	f[0]=0;
	h=1,q[t=1]=0;
	nowr=0;
	p[0].x=p[0].y=0;
	hh=1,qq[tt=1]=0;
	for(int i=1;i<=n;i++)
	{
		f[i]=inf;
		
		while(h<=t&&q[h]<ll[i]-1) h++;
		for(;nowr<rr[i]-1;)
		{
			if (nowr+1>=ll[i]-1)
			{
				++nowr;
				while(h<=t&&f[q[t]]-c*sum[q[t]]>=f[nowr]-c*sum[nowr]) t--;
				q[++t]=nowr;
			}
			else if (nowr<rr[i]-1) ++nowr;
		}
		if (ll[i]<=rr[i]&&h<=t) f[i]=min(f[i],f[q[h]]+c*(sum[i]-sum[q[h]])+d);
		
		check.x=1,check.y=2*a*sum[i];
		while(hh<tt&&multi(p[qq[hh+1]]-p[qq[hh]],check)>=0) hh++;
		f[i]=min(f[i],f[qq[hh]]+a*(sum[i]-sum[qq[hh]])*(sum[i]-sum[qq[hh]])+b);
		p[i].x=sum[i],p[i].y=a*sum[i]*sum[i]+f[i];
		while(hh<tt&&multi(p[qq[tt]]-p[qq[tt-1]],p[i]-p[qq[tt]])<=0) tt--;
		qq[++tt]=i;
		
		printf("%lld\n",f[i]);
	}
	
	return 0;
}

2017.10.24 Problem A
题目大意: 给出 m m m个区间 [ L i , R i ] [L_i,R_i] [Li,Ri],求使得用前 k k k个区间和 k k k个长度为 x x x的区间能覆盖 [ 1 , n ] [1,n] [1,n]的最小的 k k k
做法: 本题需要用到二分答案+贪心。
首先很容易看出答案具有单调性,然后对于每一个二分到的 m i d mid mid,将前 m i d mid mid个区间按左端点为第一关键字,右端点为第二关键字排序,然后从前往后贪心,如果当前区间左端点比当前覆盖到的位置要大,那么用一个长度为 x x x的区间去补,更新当前覆盖到的位置。做完之后看区间个数超不超过 m i d mid mid即可。注意特判 x = 0 x=0 x=0的情况。时间复杂度为 O ( m log ⁡ 2 m ) O(m\log^2 m) O(mlog2m)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
ll n,x,l[100010],r[100010];
int m;
struct interval {ll l,r;} s[100010];

bool cmp(interval a,interval b)
{
	if (a.l!=b.l) return a.l<b.l;
	else return a.r<b.r;
}

bool check(int mid)
{
	s[0].l=s[0].r=0;
	s[mid+1].l=s[mid+1].r=n+1;
	for(int i=1;i<=mid;i++)
		s[i].l=l[i],s[i].r=r[i];
	sort(s+1,s+mid+1,cmp);
	ll now=0,times=0,add;
	for(int i=1;i<=mid+1;i++)
	{
		if (s[i].l>now+1)
		{
			if (x==0) return 0; 
			add=((s[i].l-now-1)%x==0)?((s[i].l-now-1)/x):((s[i].l-now-1)/x+1);
			times+=add;
			now+=add*x;
		}
		now=max(now,s[i].r);
	}
	if (times>mid) return 0;
	else return 1;
}

int main()
{
	scanf("%lld%d%lld",&n,&m,&x);
	for(int i=1;i<=m;i++)
		scanf("%lld%lld",&l[i],&r[i]);
	
	int L=0,R=m+1;
	l[m+1]=r[m+1]=0;
	while(L<R)
	{
		int mid=(L+R)>>1;
		if (check(mid)) R=mid;
		else L=mid+1;
	}
	if (L==m+1) printf("Poor Douer!");
	else printf("%d",L);
	
	return 0;
}

2017.10.24 Problem B
题目大意: 要选出 n ( ≤ 1 0 9 ) n(\le 10^9) n(109)个数,这些数在 1 1 1~ m ( ≤ 1 0 9 ) m(\le 10^9) m(109)之间,要求后面的数要比前面的数大,而且某些位置上某些数不能取(这些条件有 p ( ≤ 2000 ) p(\le 2000) p(2000)个),问有多少种合法的取法。
做法: 本题需要用到Lucas定理。
f ( i , j ) f(i,j) f(i,j)为第 i i i个数选 j j j的方案数,易得状态转移方程:
f ( i , j ) = ∑ k = 1 j − 1 f ( i − 1 , j ) f(i,j)=\sum_{k=1}^{j-1} f(i-1,j) f(i,j)=k=1j1f(i1,j)
边界条件为 f ( 0 , 0 ) = 1 f(0,0)=1 f(0,0)=1,以上式子成立当且仅当第 i i i个数能选 j j j,否则 f ( i , j ) = 0 f(i,j)=0 f(i,j)=0。最后的答案显然为 ∑ i = 1 m f ( n , i ) = f ( n + 1 , m + 1 ) \sum_{i=1}^{m}f(n,i)=f(n+1,m+1) i=1mf(n,i)=f(n+1,m+1)
可是上面这个式子是 O ( n m ) O(nm) O(nm)的,显然炸到飞起,考虑优化。
注意到上述递推式可转化为 f ( i , j ) = f ( i , j − 1 ) + f ( i − 1 , j − 1 ) f(i,j)=f(i,j-1)+f(i-1,j-1) f(i,j)=f(i,j1)+f(i1,j1),觉不觉得有点像组合数的递推式?实际上,若没有数是不可取的,那么 f ( n , m ) = C m n f(n,m)=C_m^n f(n,m)=Cmn。问题是现在有了一些数是不可选的,那么我们考虑将一个 f ( i , j ) f(i,j) f(i,j)变成 0 0 0对另一个 f ( x , y ) f(x,y) f(x,y)的影响。经过分析,当 f ( i , j ) f(i,j) f(i,j)变成 0 0 0,那么 f ( x , y ) f(x,y) f(x,y)将变成 f ( x , y ) − C y − j − 1 x − i − 1 × f ( i , j ) f(x,y)-C_{y-j-1}^{x-i-1}\times f(i,j) f(x,y)Cyj1xi1×f(i,j)。所以我们将不可取的点从上到下,从左到右排序,然后一个一个变成 0 0 0,并计算它对答案以及对其他不可取点的贡献即可,时间复杂度为 O ( p 2 log ⁡ n ) O(p^2\log n) O(p2logn)
然而这样会TLE,我们可以利用Lucas定理+预处理优化时间复杂度到 O ( p 2 ) O(p^2) O(p2),详细的优化方法请看代码。
(有大佬说这题有点像APIO2016的赛艇,没做过,下次看看)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mod 1000003
using namespace std;
ll n,m,fac[1000010],inv[1000010],invf[1000010];
ll ans,now[2010];
struct point {ll a,b;} pp[2010];
int p;

ll calc_c(ll n,ll m)
{
	if (m>n) return 0;
	return fac[n]%mod*invf[m]%mod*invf[n-m]%mod;
}

ll lucas(ll n,ll m)
{
	if (n<0||m<0||m>n) return 0;
	if (m==0) return 1;
	return calc_c(n%mod,m%mod)*lucas(n/mod,m/mod)%mod; 
}

bool cmp(point a,point b)
{
	if (a.a!=b.a) return a.a<b.a;
	else return a.b<b.b;
}

int main()
{
	scanf("%lld%lld%d",&n,&m,&p);
	
	fac[0]=inv[0]=invf[0]=1;
	fac[1]=inv[1]=invf[1]=1;
	for(ll i=2;i<=1000003;i++)
	{
		fac[i]=(fac[i-1]*i)%mod;
		inv[i]=(mod-(mod/i))%mod*inv[mod%i]%mod;
		invf[i]=(invf[i-1]*inv[i])%mod;
	}

	ans=lucas(m,n);
	for(int i=1;i<=p;i++)
		scanf("%lld%lld",&pp[i].a,&pp[i].b);
	sort(pp+1,pp+p+1,cmp);
	for(int i=1;i<=p;i++)
		now[i]=lucas(pp[i].b-1,pp[i].a-1);
	for(int i=1;i<=p;i++)
	{
		ans=(((ans-now[i]*lucas(m-pp[i].b,n-pp[i].a))%mod)+mod)%mod;
		for(int j=i+1;j<=p;j++) now[j]=(((now[j]-now[i]*lucas(pp[j].b-pp[i].b-1,pp[j].a-pp[i].a-1))%mod)+mod)%mod;
	}
	printf("%lld\n",ans);
	
	return 0;
}

2017.10.24 Problem C
题目大意: 一条有 n n n个点的链,走过一条链边代价为1,有 m m m个传送门,传送门是双向的,走过没有代价,但是只能走一次,现在可以额外建造 p p p个传送门,求以任意一点为起点,任意一点为终点,走过所有链边和所有传送门所需要的最小代价。
做法: 本题需要用到贪心+堆+链表。
分析后发现题目中最后的路线是一条欧拉路,而一个无向图存在欧拉路的条件是只有一对度数为奇数的点,那么我们就需要先用 p p p个传送门删去 p p p对奇点,然后选定一对奇点作为起终点(可以看做删去),这些都是没有代价产生的,而接下来我们要连接剩下的奇点对,而这时连接一对奇点的代价为它们在链上中间的边数,这时肯定是连接从前往后第一个奇点和第二个奇点,第三个奇点和第四个奇点…这样连接是最优的。因此我们可以将问题转化为,对于一些代价 a 1 , a 2 , . . . , a t a_1,a_2,...,a_t a1,a2,...,at,从中选出若干个不相邻的元素使得最后代价最小,那么此时 O ( m 2 ) O(m^2) O(m2)的DP就很容易想出来了,不过这不是问题最后的解。
此时出题人用了一种模拟费用流退流的方法来进行贪心,具体怎么证我还不清楚,但是步骤如下:
1.找到最小的代价,累加入答案。
2.将其代价改为在链表上左右两点代价之和减去原来的代价。
3.删去其在链表上的左右两点。
于是用一个堆和一个链表维护这个贪心,就解决了这一问题,时间复杂度 O ( m log ⁡ m ) O(m\log m) O(mlogm)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#define ll long long
using namespace std;
ll inf,n,f[300010],val[300010],ans;
int m,p,pre[300010],next[300010],t=0;
bool vis[300010]={0};
struct state
{
	int id;
	ll val;
	bool operator < (state a) const
	{
		return val>a.val;
	}
};
priority_queue<state> Q;

bool cmp(ll a,ll b)
{
	return a<b;
}

void del(int x)
{
	next[pre[x]]=next[x];
	pre[next[x]]=pre[x];
	vis[x]=1;
}

int main()
{
	inf=1000000;
	inf*=inf;
	
	scanf("%lld%d%d",&n,&m,&p);
	
	f[1]=1,f[2]=n;
	for(int i=1;i<=m;i++)
	{
		ll a,b;
		scanf("%lld%lld",&a,&b);
		f[2*i+1]=a,f[2*i+2]=b;
	}
	sort(f+1,f+2*m+3,cmp);
	
	f[0]=0;
	bool last=0;
	int l=0;
	for(int i=1;i<=2*m+2;i++)
	{
		if (f[i]!=f[i-1])
		{
			if (last)
			{
				++t;
				pre[t]=t-1;
				next[t-1]=t;
				if (t>1) val[t]=f[i-1]-f[l];
				else val[t]=0;
				l=i-1;
			}
			last=0;
		}
		last=!last;
	}
	if (last)
	{
		++t;
		pre[t]=t-1;
		next[t-1]=t;
		if (t>1) val[t]=f[2*m+2]-f[l];
		else val[t]=0;
	}
	next[t]=t+1;
	pre[t+1]=t;
	val[1]=val[t+1]=inf;
	
	for(int i=2;i<=t;i++)
	{
		state now;
		now.id=i;
		now.val=val[i];
		Q.push(now);
	}
	
	ans=0;
	p=t/2-1-p;
	while(p>0)
	{
		p--;
		while(vis[Q.top().id]) Q.pop(); 
		state now=Q.top();Q.pop();
		ans+=now.val;
		val[now.id]=val[pre[now.id]]+val[next[now.id]]-val[now.id];
		del(pre[now.id]);
		del(next[now.id]);
		now.val=val[now.id];
		Q.push(now);
	}
	printf("%lld",n-1+ans);
	
	return 0;
}

2017.10.25 Problem A
题目大意: 给定一个长为 n ( ≤ 300000 ) n(\le 300000) n(300000)的字符串 A A A和一个长为 m ( ≤ 200 ) m(\le 200) m(200)的字符串 B B B,问有多少个区间 [ l , r ] [l,r] [l,r]满足 A A A在这个区间内包含和 B B B相同的子串。
做法: 本题需要用到DP计数。
f ( i , j ) f(i,j) f(i,j)为最大的 k k k使得 A [ k , i ] A[k,i] A[k,i]内包含 B [ 1 , j ] B[1,j] B[1,j]这个子串,易得状态转移方程:
f ( i , j ) = m a x ( f ( i − 1 , j − 1 ) , f ( i − 1 , j ) ) f(i,j)=max(f(i-1,j-1),f(i-1,j)) f(i,j)=max(f(i1,j1),f(i1,j)) ( A [ i ] = B [ j ] ) (A[i]=B[j]) (A[i]=B[j])
f ( i , j ) = f ( i − 1 , j ) f(i,j)=f(i-1,j) f(i,j)=f(i1,j) ( A [ i ] ≠ B [ j ] ) (A[i]\ne B[j]) (A[i]=B[j])
j = 1 j=1 j=1的时候特判一下即可。这个状态转移方程是 O ( n m ) O(nm) O(nm)的,可以接受。可是算出来这个东西有什么用呢?注意到 f ( i , m ) f(i,m) f(i,m)的意义,可以知道区间 [ 1 , i ] , [ 2 , i ] , . . . , [ f ( i , m ) , i ] [1,i],[2,i],...,[f(i,m),i] [1,i],[2,i],...,[f(i,m),i]都满足题目条件,因此我们累加所有的 f ( i , m ) f(i,m) f(i,m)就是答案。求的时候可以用滚动数组将空间优化到 O ( m ) O(m) O(m)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
ll n,m,f[2][210],ans,now,past;
char a[300010],b[210];

int main()
{
	scanf("%s",a);
	scanf("%s",b);
	n=strlen(a),m=strlen(b);
	
	ans=0,now=0,past=1;
	memset(f,0,sizeof(f));
	for(ll i=0;i<n;i++)
	{
		for(ll j=0;j<m;j++)
		{
			if (a[i]==b[j])
			{
				if (j==0) f[now][j]=i+1;
				else f[now][j]=f[past][j-1];
			}
			else f[now][j]=f[past][j];
		}
		ans+=f[now][m-1];
		swap(now,past);
	}
	
	printf("%lld",ans);
	
	return 0;
}

2017.10.25 Problem B
题目大意: 对一个 1 1 1~ n n n的排列,用冒泡排序生成一个有 n n n个点的无向图,步骤是:
枚举 i i i 1 1 1 n n n
枚举 j j j i + 1 i+1 i+1 n n n
如果 a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j]:添加无向边 ( a [ i ] , a [ j ] ) (a[i],a[j]) (a[i],a[j]),交换 a [ i ] , a [ j ] a[i],a[j] a[i],a[j]
求生成出的无向图的最大独立集大小,而且求出有哪些点必然在最大独立集中。
做法: 本题需要用到DP求解最长上升/下降子序列。
注意到上述算法是在给所有逆序对之间连边,即所有满足 i < j i<j i<j并且 a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j]的点对 ( i , j ) (i,j) (i,j)之间都连有边,而最大独立集是需要满足集合中两两间都没有边,即所有点对 ( i , j ) (i,j) (i,j)都满足 a [ i ] < a [ j ] a[i]<a[j] a[i]<a[j],注意到一个独立集就对应一个上升子序列,那么求最大独立集显然就是求最长上升子序列了,用 O ( n log ⁡ n ) O(n\log n) O(nlogn)的DP做法即可。
然而怎么判断一个点一不一定在最长上升子序列中呢?我们令 f ( i ) f(i) f(i)为以 a [ i ] a[i] a[i]结尾的最长上升子序列长度, g ( i ) g(i) g(i)为以 a [ i ] a[i] a[i]开头的最长上升子序列长度(其实就是反着求最长下降子序列),已求出的最长上升子序列长度为 a n s ans ans,那么若 f ( i ) + g ( i ) − 1 = a n s f(i)+g(i)-1=ans f(i)+g(i)1=ans,则 a [ i ] a[i] a[i]可能在最长上升子序列中,而且它必然在最长上升子序列的第 f ( i ) f(i) f(i)个位置,因此我们只要记录最长上升子序列的每个位置是不是唯一的,如果是唯一的,那么那个点就一定在最长上升子序列中。时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n,a[100010],f[100010],g[100010],d[100010],top,num[100010]={0};

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	
	top=d[0]=0;
	for(int i=1;i<=n;i++)
	{
		int l=0,r=top;
		if (d[top]<=a[i]) f[i]=++top,d[top]=a[i];
		else
		{
			while(l<r)
			{
				int mid=(l+r)>>1;
				if (d[mid]<=a[i]) l=mid+1;
				else r=mid;
			}
			f[i]=l,d[l]=a[i];
		}
	}
	printf("%d\n",top);
	
	memset(d,0,sizeof(d));
	top=0,d[0]=n+1;
	for(int i=n;i>=1;i--)
	{
		int l=0,r=top;
		if (d[top]>=a[i]) g[i]=++top,d[top]=a[i];
		else
		{
			while(l<r)
			{
				int mid=(l+r)>>1;
				if (d[mid]>=a[i]) l=mid+1;
				else r=mid;
			}
			g[i]=l,d[l]=a[i];
		}
	}
	
	for(int i=1;i<=n;i++)
		if (f[i]+g[i]-1==top) num[f[i]]++;
	for(int i=1;i<=n;i++)
		if (f[i]+g[i]-1==top&&num[f[i]]==1) printf("%d ",i);
	
	return 0;
}

2017.10.25 Problem C
题目大意: 有一个带正边权的有向图,两个人分别从点 s 1 , s 2 s_1,s_2 s1,s2出发,分别沿最短路走到 t 1 , t 2 t_1,t_2 t1,t2,问他们最多能走过多少公共点。
做法: 本题需要用到最短路DAG+DAG最长路。
首先我们可以求出从点 s 1 , s 2 s_1,s_2 s1,s2出发的单源最短路,可能在最短路上的边形成一个DAG(有向无环图),我们把它叫做最短路DAG,可以证明两个最短路DAG的交(这里指边集的交,下同)一定也是一个DAG,这个用反证法应该可以证。因此,我们只需要求这个DAG上能经过的最多的点数,也就是DAG最长路即可。然而我们需要注意两个DAG没有交的情况,这时候我们就找可能同时在两条最短路上的点,如果有这样的点答案就是 1 1 1,否则答案为 0 0 0。数据卡SPFA,用Dijkstra算法即可,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)
以下是本人代码:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#define ll long long
using namespace std;
int n,m,b=0,s1,t1,s2,t2,tot=0,first[50010][2]={0},firstq[50010]={0},in[50010]={0},f[50010]={0},le=0,mx=0;
ll inf,dis[50010],dist[50010][5];
bool vis[50010];
queue<int> qq;
struct point
{
	int v;
	ll val;
	bool operator < (point a) const
	{
		return val>a.val;
	}
};
priority_queue<point> Q;
struct edge {int v,next;ll d;} e[200010][2],q[200010];

void insert(int a,int b,ll d)
{
	e[++tot][0].v=b;
	e[tot][0].next=first[a][0];
	e[tot][0].d=d;
	first[a][0]=tot;
	e[tot][1].v=a;
	e[tot][1].next=first[b][1];
	e[tot][1].d=d;
	first[b][1]=tot;
}

void insertq(int a,int b)
{
	q[++tot].v=b;
	q[tot].next=firstq[a];
	firstq[a]=tot;
	in[b]++;
}

void dijkstra(int s,int mode,int to)
{
	point now,next;
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++)
	{
		if (i!=s) dis[i]=inf;
		else dis[i]=0;
	}
	for(int i=first[s][mode];i;i=e[i][mode].next)
		dis[e[i][mode].v]=min(dis[e[i][mode].v],e[i][mode].d);
	for(int i=1;i<=n;i++)
		if (i!=s)
		{
			now.v=i,now.val=dis[i];
			Q.push(now);
		}
	for(int i=1;i<n;i++)
	{
		now=Q.top(),Q.pop();
		while(vis[now.v]) now=Q.top(),Q.pop();
		vis[now.v]=1;
		for(int j=first[now.v][mode];j;j=e[j][mode].next)
			if (!vis[e[j][mode].v]&&dis[e[j][mode].v]>dis[now.v]+e[j][mode].d)
			{
				next.v=e[j][mode].v;
				next.val=dis[next.v]=dis[now.v]+e[j][mode].d;
				Q.push(next);
			}
	}
	for(int i=1;i<=n;i++) dist[i][to]=dis[i];
}

int main()
{
	inf=1000000000;
	inf*=inf;
	
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;ll d;
		scanf("%d%d%lld",&u,&v,&d);
		insert(u,v,d);
	}
	scanf("%d%d%d%d",&s1,&t1,&s2,&t2);
	
	dijkstra(s1,0,1);
	dijkstra(s2,0,2);
	dijkstra(t1,1,3);
	dijkstra(t2,1,4);
	if (dist[t1][1]==inf||dist[t2][2]==inf) {printf("-1");return 0;}
	tot=0;
	for(int i=1;i<=n;i++)
		for(int j=first[i][0];j;j=e[j][0].next)
			if (dist[i][1]+dist[e[j][0].v][3]+e[j][0].d==dist[t1][1]
				&&dist[i][2]+dist[e[j][0].v][4]+e[j][0].d==dist[t2][2]) insertq(i,e[j][0].v),le++;
	for(int i=1;i<=n;i++)
		if (dist[i][1]+dist[i][3]==dist[t1][1]&&
		dist[i][2]+dist[i][4]==dist[t2][2]) {b=1;break;}
	
	for(int i=1;i<=n;i++)
		if (!in[i]) qq.push(i);
	while(!qq.empty())
	{
		int v=qq.front();qq.pop();
		for(int i=firstq[v];i;i=q[i].next)
		{
			in[q[i].v]--;
			f[q[i].v]=max(f[q[i].v],f[v]+1);
			if (!in[q[i].v]) qq.push(q[i].v);
		}
	}
	for(int i=1;i<=n;i++) mx=max(mx,f[i]);
	printf("%d",le?mx+1:b);
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值