写在题解前的一点废话:
这次考试前面两道题都很快的写完了正解并拍完了,只有最后一道题迟迟没有想到orz明白正解后恍然大悟,优先级真是个神奇的东西【点头
第一题:
马拉松接力赛(race)
【题目大意】
告诉你共有n个人,每个人跑了多少米,再给你q个询问,问你第i米是第几个人在跑
【样例输入】
3 5
2 1 3
2 3 4 0 1
【样例输出】
2
3
3
1
1
【思路】
对于70%以下的数据,都可以一点点暴力用桶排完成。
对于100%的数据,有两种比较常见的解法。
一是先维护第i个人从第j米开始跑,然后询问用二分查找找到这个米数的位置,然后就能找到这个米数是哪个人在跑。
二可以离线,先把询问按照米数大小排好序,维护一个当前跑到了第几米,再用一个类似指针的东西,一点点指向每个询问,如果当前米数大于这个询问米数了,那么这个询问的答案找到了。
时间复杂度只有O(logn*n+logq*q)。
【要注意的问题】
1)这道题不是很难,如果是二分的话要注意区分上下区间,不如说整个这道题都要注意选手跑的区间是左闭右开的
2)注意询问中可能有第0米,如果处理不好的话很可能会输出0
3)如果是离线做法的话,get到答案之后要记得再按询问顺序排以一下序
【代码实现】
#include <stdio.h>
#include <algorithm>
using namespace std;
#define MAXN 50005
struct A
{
int no,ans,sk;
}ask[MAXN];
int num[MAXN];
bool cmp1(A a,A b)
{
return a.sk<b.sk;
}
bool cmp2(A a,A b)
{
return a.no<b.no;
}
int main()
{
freopen("race.in","r",stdin);
freopen("race.out","w",stdout);
int n,q;
scanf("%d%d",&n,&q);
for(int i = 1;i<= n;i++)
scanf("%d",&num[i]);
int w;
for(int i = 1;i<= q;i++)
{
scanf("%d",&w);
ask[i].sk = w;
ask[i].no = i;
}
sort(ask+1,ask+q+1,cmp1);
int now = 0,p = 0;
for(int i = 1;i<= q;i++)
{
while(p<=n&&now<=ask[i].sk)
{
p++;
now = now + num[p];
}
ask[i].ans = p;
}
sort(ask+1,ask+q+1,cmp2);
for(int i = 1;i<= q;i++)
{
printf("%d\n",ask[i].ans);
}
return 0;
}
第二题:
拔河(tug)
【题目大意】
现在有一群人,每个人都有一个战斗力xi,但是这些人中每隔k个人必须出队一个,问剩下的人战斗力最高能是多少
【样例输入】
5 2
1 2 3 4 5
【样例输出】
12
【思路】
题目里问剩下的人战斗力最高可以是多少,我们可以反过来想,想问出队的人战斗力最少可以是多少,最后我们再用总的战斗力减去这个最小出队战斗力就为最大剩余战斗力。
设状态F[i]为,若i这个人出队的话,最小出队战斗力可以是多少
则状态转移方程为F[i]=min(F[i-1],F[i-2]……,F[i-k-1])+x[i]。
其实说白了就是在前1个到前k+1个状态中取一个最小的,然后再加上当前战斗力。
如果是单纯的动态规划,那么时间复杂度约为O(
n2
),或者说O(n*k)可以过60%的数据
然后我们可以简单地发现,找到这个最小值得过程我们可以选择用单调队列维护。
这样我们的时间复杂度就降到了O(n)
于是我们就把这题转化成了一个单调队列的模型题烽火传递(jdoj1056)
【要注意的问题】
1)单调队列注意两端的开闭性
2)注意转移方程取最小值是前1个到前k+1个状态中最小的一个,因为中间最多可以保留k个人,而状态设置的是若第i个人出队。
【代码实现】
#include <stdio.h>
#include <algorithm>
using namespace std;
#define N 100005
long long f[N];
int que[N];
int l,r;
int main()
{
freopen("tug.in","r",stdin);
freopen("tug.out","w",stdout);
int n,k,x;
long long sum = 0;
scanf("%d%d",&n,&k);
que[r++] = 0;
for(int i = 1;i<= n;i++)
{
scanf("%d",&x);
sum = sum+x;
while(l<r&&que[l]<i-k-1)
l++;
f[i] = f[que[l]]+x;
while(l<r&&f[que[r-1]]>f[i])
r--;
que[r++] = i;
}
long long ans = 1;
ans = ans<<62;
for(int i = n-k;i<= n;i++)
{
ans = min(ans,f[i]);
}
printf("%I64d",sum-ans);
return 0;
}
第三题:
修路(road/bzoj2654)
【题目大意】
给你一个连通图,每一条边的编号都是0或者1,现在想让你求一个有k条0号边的最小生成树。
【样例输入】
2 2 1
1 2 1 1
1 2 2 0
【样例输出】
2
【思路】
考试的时候第一时间想到的是一个贪心,总之先把0号边中找k条最小的出来,然后在连1号边,做一个最小生成树。然而这个算法很明显有反例,但是也算是得了30分。正确做法是给0号边设置一个优先级,每次把0号边的边权加上这个优先级,优先级越小,选用的0号边就越多,于是我们可以二分这个优先级,直到找到一个优先级使它刚好选用了k条0号边。
算法的时间复杂度约为 O(
log2m∗m
)
【要注意的问题】
1)由于要做多次krus,所以要注意并查集的初始化。
2)由于是每次都加上了不一样的优先级,所以不能只排一次序,而是每次krus都要重新排序。
3)把边的信息都存在数组里每次加优先级的时候调用比起直接在边权上修改会方便一点。
【代码实现】
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
#define M 100005
struct E
{
int x,y,val,d;
}edge[M];
int fa[50005];
int v[M],xi[M],yi[M],di[M];
int n,m,k;
int getfa(int x)
{
if(fa[x]==x||!fa[x])return fa[x] = x;
else return fa[x] = getfa(fa[x]);
}
void uni(int x,int y)
{
int fx = getfa(x),fy = getfa(y);
if(fx!=fy)
fa[fx] = fy;
}
bool cmp1(E a,E b)
{
if(a.val!=b.val)return a.val<b.val;
else return a.d<b.d;
}
long long ans;
bool krus(int w)
{
for(int i = 1;i<= m;i++)
{
edge[i].x = xi[i];
edge[i].y = yi[i];
edge[i].val = v[i];
edge[i].d = di[i];
if(di[i]==0)edge[i].val+=w;
}
sort(edge+1,edge+1+m,cmp1);
ans = 0;
int totk = 0;
memset(fa,0,sizeof(fa));
for(int i = 1;i<= m;i++)
{
int fx = getfa(edge[i].x);
int fy = getfa(edge[i].y);
if(fx==fy)continue;
if(edge[i].d==0)totk++;
ans+=edge[i].val;
uni(fx,fy);
}
return totk>=k;
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i = 1;i<= m;i++)
{
scanf("%d%d%d%d",&xi[i],&yi[i],&v[i],&di[i]);
xi[i]++;
yi[i]++;
}
int l = -105,r = 105,mid;
while(l<=r)
{
mid = (l+r)>>1;
if(krus(mid)){l = mid+1;ans = ans-k*mid;}
else r = mid-1;
}
printf("%I64d",ans);//其实没有必要用long long 但是以防万一嘛~
return 0;
}