51Nod 试题泛做1
A-排船的问题
很显然,这个数据范围用二分来找这个最长的最短是OK的,
然后我们就判断一下二分到的东西,用一个贪心,就是尽可能将每一个往左边放,但不能与船重叠,也不能超过我们二分到的最长的绳的长度,因为要尽可能给后面留出空间,让后面的绳的长度不超过我们二分到的长度。
然后如果最后极限的方法仍飞出了港口,那自然就是不行的,就继续二分。
可以这样理解。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e4+5;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int n,x,m;
int p[MAXN];
inline bool check(int pos)
{
int head=0,tail=0;
for(int i=1;i<=n;i++)
{
head=tail;
if(head+x>p[i]+pos) return false;
tail=max(head+2*x,p[i]-pos+x);
}
if(tail>m) return false;
return true;
}
int main()
{
n=read(),x=read(),m=read();
for(int i=1;i<=n;i++) p[i]=read();
if((x*2*n)>m) {printf("-1\n");return 0;}
int l=0,r=m-1,ans;
while(l<=r)
{
int mid=(l+r)>>1;
if(check(mid))ans=mid,r=mid-1;
else l=mid+1;
}
printf("%d\n",ans);
return 0;
}
B-稳定桌
首先,我们看复杂度,题目数据范围是 \(1\leqslant n \leqslant 10^5\),那么一种 \(\mathcal{O}(n\log n)\) 会是比较可能的。
然后分析题目可以想到一个方法:从小到大枚举我们要留下的最长桌腿的长度 \(i\),将长度大于 \(i\) 的桌腿全删掉,此时考虑一下是否删够了,即我们枚举到的 \(i\) 的数量是否超过了剩下数量的一半,超过就直接更新答案好了,没满足就还得从比 \(i\) 小的里面删。
然后就来到了本题的关键部分:
该如何筛选比 \(i\) 小的桌腿呢?
显然,若假设还需删 \(k\) 条,那么肯定是在小的里面找消耗能量前 \(k\) 小的删掉。教练第一反应是平衡树,但太难写也太难调,所以我们这里考虑到用权值线段树。
取所有代价中的最大值 \(maxd\),以 \(1\sim maxd\) 为值域建立权值线段树,在此过程中我们要记录两个量,\(sum_i\) 是子树中所有代价之和,\(num_i\) 是子树中桌腿的数量。
然后我们要保证有序,就在枚举的同时,将每一次枚举完的桌腿都插到树里,将小的往左插,大的往右插。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;
const int INF=0x7f7f7f7f7f7f7f7f;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int n;
vector<int>V[MAXN];
int t[MAXN<<2],num[MAXN<<2];
inline void pushup(int p)
{
t[p]=t[p<<1]+t[p<<1|1];
num[p]=num[p<<1]+num[p<<1|1];
return;
}
inline void change(int p,int l,int r,int k)
{
if(l==r)
{
num[p]++,t[p]+=k;
return;
}
int mid=(l+r)>>1;
if(k<=mid) change(p<<1,l,mid,k);
else change(p<<1|1,mid+1,r,k);
pushup(p);
}
inline int ask(int p,int l,int r,int k)
{
if(l==r) return l*k;
int mid=(l+r)>>1;
if(num[p<<1]==k) return t[p<<1];
else if(num[p<<1]>k) return ask(p<<1,l,mid,k);
else return t[p<<1]+ask(p<<1|1,mid+1,r,k-num[p<<1]);
}
int l[MAXN],d[MAXN];
signed main()
{
n=read();int maxl=-1,maxd=-1;int ans=INF,sum=0,cnt=0,add;
for(int i=1;i<=n;i++) l[i]=read(),maxl=max(maxl,l[i]);
for(int i=1;i<=n;i++)
{
d[i]=read();maxd=max(maxd,d[i]);
V[l[i]].push_back(d[i]);sum+=d[i];
}
for(int i=1;i<=maxl;i++)
if(V[i].size())
{
cnt+=V[i].size();
for(int j=0;j<V[i].size();j++) sum-=V[i][j];
if(cnt>V[i].size()) add=ask(1,1,maxd,cnt-V[i].size()*2+1);
else add=0;
ans=min(ans,sum+add);
for(int j=0;j<V[i].size();j++) change(1,1,maxd,V[i][j]);
}
printf("%lld\n",ans);
return 0;
}
C-金牌赛事
这是一道线段树优化DP。
设 \(dp_i\) 表示前 \(i\) 条道路修若干条可得到的最大利润,那么我们枚举一个 \(j\),则 \(i\) 从两种情况转移:
1.若第 \(i\) 条不修,则 \(dp_i=dp_{i-1}\);
2.若第 \(i\) 条要修,则 \(dp_i=dp_{j}+val_{j+1,i}-cost_{j+1,i}\);
然而显然现在 \(\mathcal{O}(n^2)\) 是不够的,且 \(val\) 的处理也很麻烦,耗费时间和空间巨量。
所以我们来观察一下,第二种情况的转移是从一段区间里选最大值,那么我们可以用线段树优化成 \(\mathcal{O}(\log n)\)。
同时,对于在线段树上算贡献也更好操作:
1.将每一个赛道右端点对应的左端点与贡献存在 vector 里面;
2.在往右扫到第 \(i\) 个时,在线段树上将区间 \([0,i-1]\) 都剪掉花费 \(c_i\);
3.在往右扫到第 \(i\) 个时,将右端点为 \(i\) 的赛道挑出来,其左端点为 \(l\),贡献为 \(p\),在线段树上将区间 \([0,l-1]\) 加上贡献 \(p\);
4.在每次枚举结束后,将 \(dp_i\) 加到线段树上对应的位置 \(i\) 上。
区间修改,区间查询,时间复杂度 \(\mathcal{O}(n\log n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;
const int INF=0x7f7f7f7f7f7f7f7f;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
struct Node
{
int l,p;
};
int t[MAXN<<2],dat[MAXN<<2];
inline void pushup(int p)
{
t[p]=max(t[p<<1],t[p<<1|1]);
return;
}
inline void addtag(int p,int l,int r,int k)
{
t[p]+=k,dat[p]+=k;
return;
}
inline void pushdown(int p,int l,int r)
{
if(dat[p])
{
int mid=(l+r)>>1;
addtag(p<<1,l,mid,dat[p]);
addtag(p<<1|1,mid+1,r,dat[p]);
dat[p]=0;
}
return;
}
inline void change(int p,int l,int r,int a,int b,int k)
{
if(l>=a && r<=b)
{
addtag(p,l,r,k);
return;
}
int mid=(l+r)>>1;
pushdown(p,l,r);
if(a<=mid) change(p<<1,l,mid,a,b,k);
if(b>mid) change(p<<1|1,mid+1,r,a,b,k);
pushup(p);
return;
}
inline int ask(int p,int l,int r,int a,int b)
{
if(l>=a && r<=b) return t[p];
int mid=(l+r)>>1,ans=-INF;
if(a<=mid) ans=max(ans,ask(p<<1,l,mid,a,b));
if(b>mid) ans=max(ans,ask(p<<1|1,mid+1,r,a,b));
return ans;
}
int dp[MAXN],c[MAXN];
int n,m;
vector<Node>V[MAXN];
signed main()
{
n=read(),m=read();
for(int i=1;i<=n;i++) c[i]=read();
for(int i=1;i<=m;i++)
{
int L=read(),R=read(),P=read();
V[R].push_back({L,P});
}
for(int i=1;i<=n;i++)
{
change(1,0,n,0,i-1,-c[i]);
for(int j=0;j<V[i].size();j++)
{
int l=V[i][j].l,p=V[i][j].p;
change(1,0,n,0,l-1,p);
}
dp[i]=max(dp[i-1],ask(1,0,n,0,i-1));
change(1,0,n,i,i,dp[i]);
}
printf("%lld\n",dp[n]);
return 0;
}
D-公园晨跑
首先看到环,就得断环成链。
要求最大值,考虑分 $$两部分分别最大化
距离可以前缀和预处理出来,那么就变成 \(sum_y-sum_x+2h_x+2h_y\),为了好处理将相同位置的值放一起然后加括号 \(sum_y+2h_y-(sum_x-2h_x)\)。
那么就变成了两个 RMQ 问题,没有修改,用线段树维护可行。
但是有一个重要的点,就是有可能出现查到的最大与最小的位置是同一个,这不符合要求,所以我们线段树维护的是下标而不是具体算出来的值,这方便最后处理时,若有重合,那么就往 \([l,pos-1],[pos+1,r]\) 左右两个区间去查,这里 \(pos\) 是重合的那个下标。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;
const int INF=0x7f7f7f7f7f7f7f7f;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int n,m;
int d[MAXN],h[MAXN];
int maxn[MAXN<<2],minn[MAXN<<2];
int summax[MAXN],summin[MAXN];
inline int getmax(int x,int y)
{
return summax[x]>summax[y]?x:y;
}
inline int getmin(int x,int y)
{
return summin[x]<summin[y]?x:y;
}
inline void pushup(int p)
{
maxn[p]=getmax(maxn[p<<1],maxn[p<<1|1]);
minn[p]=getmin(minn[p<<1],minn[p<<1|1]);
return;
}
inline void build(int p,int l,int r)
{
if(l==r)
{
maxn[p]=minn[p]=l;
return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid);build(p<<1|1,mid+1,r);
pushup(p);
return;
}
inline int askmax(int p,int l,int r,int a,int b)
{
if(l>=a && r<=b) return maxn[p];
int mid=(l+r)>>1,ans=0;
if(a<=mid) ans=askmax(p<<1,l,mid,a,b);
if(b>mid) ans=getmax(ans,askmax(p<<1|1,mid+1,r,a,b));
return ans;
}
inline int askmin(int p,int l,int r,int a,int b)
{
if(l>=a && r<=b) return minn[p];
int mid=(l+r)>>1,ans=0;
if(a<=mid) ans=askmin(p<<1,l,mid,a,b);
if(b>mid) ans=getmin(ans,askmin(p<<1|1,mid+1,r,a,b));
return ans;
}
inline int calcmax(int x,int y)
{
if(x>y) return 0;
return askmax(1,1,n*2+1,x,y);
}
inline int calcmin(int x,int y)
{
if(x>y) return 0;
return askmin(1,1,n*2+1,x,y);
}
inline int solve(int x,int y)
{
int maxi=calcmax(x,y),mini=calcmin(x,y);
if(mini!=maxi) return summax[maxi]-summin[mini];
int maxx=getmax(calcmax(x,maxi-1),calcmax(maxi+1,y));
int minx=getmin(calcmin(x,mini-1),calcmin(mini+1,y));
return max(summax[maxi]-summin[minx],summax[maxx]-summin[mini]);
}
signed main()
{
n=read(),m=read();
for(int i=1;i<=n;i++) d[i+1]=d[i+n+1]=read();
for(int i=1;i<=n;i++) h[i]=h[i+n]=read();
int sum=0;summax[0]=-INF,summin[0]=INF;
for(int i=1;i<=n*2;i++) sum+=d[i],summax[i]=sum+2*h[i],summin[i]=sum-2*h[i];
build(1,1,n*2+1);
for(int i=1;i<=m;i++)
{
int l=read(),r=read();
if(l<=r) printf("%lld\n",solve(r+1,n+l-1));
else printf("%lld\n",solve(r+1,l-1));
}
return 0;
}
/*
sum[y]+2*h[y]取max sum[x]-2*h[x]取min
*/
E-幸运树
看到幸运边,考虑如何特殊处理。
可以将所有幸运边断开,形成若干个(假设是 \(m\))连通块 \(t_1,t_2,\dots,t_m\),然后有一个显然的结论:两个不同连通块的点之间一定有幸运边。
因为没有要求方案,所以我们就直接用公式算:
\(ans_i=\sum\limits_{i=1}^m|t_i|\binom{n-|t_i|}{2}\)
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') f=-1;
ch=getchar();
}
while(isdigit(ch))
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*f;
}
int n;
struct edge
{
int to,nxt,len;
}e[MAXN<<1];
int head[MAXN],cnt;
inline void add(int x,int y,int z)
{
e[++cnt].to=y;
e[cnt].len=z;
e[cnt].nxt=head[x];
head[x]=cnt;
return;
}
inline bool check(int x)
{
int xx=x;
while(xx)
{
int pos=xx%10;xx/=10;
if(pos!=4 && pos!=7) return false;
}
return true;
}
bool vis[MAXN];
int cntt;
inline void dfs(int x)
{
vis[x]=true;cntt++;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,z=e[i].len;
if(vis[y] || z) continue;
dfs(y);
}
return;
}
signed main()
{
n=read();
for(int i=1;i<=n-1;i++)
{
int x=read(),y=read(),z=read();
int w=check(z);
add(x,y,w),add(y,x,w);
}
int ans=0;
for(int i=1;i<=n;i++)
if(!vis[i])
{
cntt=0;dfs(i);
ans+=cntt*(n-cntt)*(n-cntt-1);
}
printf("%lld\n",ans);
return 0;
}
F-犯罪计划
点击查看代码