在学校模拟赛中出现了一些比较有意思的题目,在这里写出来总结一下。一篇博客就写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}}
原式=2n−11+∑i=2n2n−i+1i=2n−11+∑i=2n2n−1i2i−2=2n−12n−1+∑i=0n−2(2n−1−2i)=n−1+2n−11
至于倒数第二个式子是怎么推出来的,可以手画个矩阵找规律,第一列写
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}
2n−2,我们发现我们要求的就是这一堆东西的和除以
2
n
−
1
2^{n-1}
2n−1,然后我们知道每一行和的和等于每一列和的和,就可以推出上面的式子了。然后用快速幂和逆元处理一下即可。时间复杂度
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=1j−1f(i−1,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,j−1)+f(i−1,j−1),觉不觉得有点像组合数的递推式?实际上,若没有数是不可取的,那么
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)−Cy−j−1x−i−1×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(i−1,j−1),f(i−1,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(i−1,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;
}