基础算法1
离散化
就是把无限空间(在OI中就是很大的范围)里面的信息,映射到一个较小的空间里面
有时候需要保证仍然保留了一些信息,比如元素之间的大小关系,比如相邻两个元素的差(去重w)
一个对闭合区间离散化的小技巧
有若干个区间$[L_i,R_i] $,把他们离散化成若干个区间:
如何划分?
集合Sp表示覆盖这个点p的区间编号
将数轴上的点划分成n个区间,每个区间的点等价(区间内的点被覆盖的区间相同,也就是集合Sp相同);
举个例子:
[1,3]------①
[2,5]------②
那么划分为三个区间:
[1,1]={①};
[2,3]={①,②};
[4,5]={②}
如何实现?
1.将所有的$L_i,R_i+1 $都拿出来排序(排序时不考虑L与R的区别);
我们设排序去重后的数组为V;
那么相邻两个元素可以得到一个区间$[V_i,V_{i+1}-1] $
而得到的每一个区间,也就像我们上面↑举的例子所划分的三个区间一样的;
如何去重:
sort(V + 1, V + 1 + N);
M = unique(V + 1, V + 1 + N) - (V + 1);
如何应用?
举个栗子:
假设读入区间为$[1,107],[105+1,10^9] $
那么我们划分的区间就是:
\([1,10^5]\) ---------①
\([10^5+1,10^7]\)-----------②
\([10^7+1,10^9]\)-----------③
这样,对于读入的两个区间,就可以映射为:
$[1,10^7]=>[①,②] $
$[105+1,109]=>[②,③] $
前缀和和差分
什么是前缀和?
对于一个数组A,记录S i = A[1]+ A[2]+ ⋯ +A[i]
显然S[i]= S[i−1]+A[i],所以S数组可以线性递推出来
那么A[l]+ A[l+1]+ ⋯ +A[r]= S[r]− S[l−1]
这样就可以把,求一个数组中一段区间的和这个O(r − l)的事情变
成O(1)的啦
实际上,只要有可加可减性的信息都可以这么搞
求出前缀积、前缀异或和等等
什么是差分?
对一个数组A进行差分,形式化而言就是:D[i]= A[i]−A[i−1]
对原序列A的区间[L, R]进行+1等价于D[L]+= 1, D[R+1]−= 1
那么可以直接维护D
用D还原A也是轻松的,差分的逆运算是前缀和,直接对D做前缀和即可还原A
洛谷P3406
据说是一道差分模板题,我们可以记录每一条铁路经过的次数,如果O(n^2)的去加,显然会超时(n超大),那么我们可以考虑差分数组。
因为第i段铁路表示的是第i个城市~第i+1个城市,所以对于一段x=>y,我们只需要在两个中较小的对应的差分数组+1,较大的-1,然后对每一段铁路,贪心的比较是\(c_i+b_i*经过次数\)与$a_i*经过次数 $的大小关系,取较小一个加进ans里;
这样处理就好了√;
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-') ans=-ans;
return ans;
}
ll n,m;
ll d[100010];
ll a,b,c;
int main(){
n=read();
m=read();
ll p,nxt;
ll z,y;
for(int i=1;i<=m;i++) {
p=read();
if(i==1) {
nxt=p;
continue;
}
y=min(p,nxt);
z=max(p,nxt);
d[y]++;
d[z]--;
nxt=p;
}
ll ans=0;
for(int i=1,x=0;i<n;i++) {
a=read();
b=read();
c=read();
x+=d[i];
if(c+b*x<a*x) ans+=c+b*x;
else ans+=a*x;
}
printf("%lld",ans);
return 0;
}
洛谷P1115
之前做这道题好像是用dp做的,转移方程大概是dp[i]=max{dp[i-1]+a[i],a[i]};
现在尝试用前缀和做,也就是找一对l,r,使得sum[r]-sum[l]最大,从左往右维护sum[i]的最小值,然后与sum[i]相减(注意不可以sum[i]-sum[i]),求最大(好乱)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-') ans=-ans;
return ans;
}
ll n,ans;
ll sum[200010];
int main(){
n=read();
ll minn=21474836470000;
for(int i=1,x;i<=n;i++) {
x=read();
sum[i]=sum[i-1]+x;
}
ans=sum[1];
minn=sum[1];
ll y;
for(int i=2;i<=n;i++) {
y=max(sum[i],sum[i]-minn);
if(sum[i]<minn) minn=sum[i];
ans=max(ans,y);
}
printf("%lld",ans);
return 0;
}
洛谷P3397
二维前缀和与二维差分:
\(s[x][y]=s[x][y-1]+s[x-1][y]-s[x-1][y-1]+a[x][y]\)
二维差分:
对于(x1,y1)~(x2,y2) 的区间A+1;
等价于:
差分数组D:\(D[x1][y2+1]-=1 \ \ D[x2+1][y1]-=1 \ \ D[x2+1][y2+1]+=1 \ \ D[x1][y1]+=1;\)
for (x = 1 ~ N)
for (y = 1 ~ M)
S[x][y] = S[x - 1][y] + S[x][y - 1] - S[x - 1][y - 1] + A[x][y];
//表示不懂下面在干什么:
for (x = 1 ~ N)//对每一行做一个一维前缀和
for (y = 1 ~ M)
S[x][y] = S[x][y - 1] + A[x][y];
for (y = 1 ~ M)//对列做前缀和
for (x = 1 ~ N)
S[x][y] += S[x - 1][y];
维护二维数组D,然后最后做二维前缀和:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-') ans=-ans;
return ans;
}
int n,m;
int D[1010][1010],s[1010][1010];
int main(){
n=read();
m=read();
int x1,y1,x2,y2;
for(int i=1;i<=m;i++) {
x1=read();y1=read();
x2=read();y2=read();
D[x1][y1]+=1;
D[x2+1][y2+1]+=1;
D[x1][y2+1]-=1;
D[x2+1][y1]-=1;
}
for(int x=1;x<=n;x++)
for(int y=1;y<=n;y++)
s[x][y]=s[x-1][y]+s[x][y-1]-s[x-1][y-1]+D[x][y];
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++)
printf("%d ",s[i][j]);
puts("");
}
return 0;
}
树上差分:
对边差分:
(u,v)全部加上w,对于差分数组D就是:
D[u]+=w;D[v]+=w;D[lca(u,v)]-=2*w;
用子树中差分数组的和来还原信息:即将子树中所有的D[i] i∈son(r) 相加;
每个点的信息记录的是其到父亲的边的信息
对点差分:
(u,v)全部加上w,对于差分数组D就是:
D[u]+=w;D[v]+=w;D[lca(u,v)]-=w;Fatherlca-=w;
感性李姐
洛谷P3258
利用上面对点差分的思想,进行加减操作,注意因为下一段路径的起点是上一段路径的终点,所以除a[1]外,其他经过的点的值要顺次减一。求和是做子树和不是树上前缀和。
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-' ) ans=-ans;
return ans;
}
const int mxn=300010;
int n,ecnt;
int a[mxn],head[mxn],d[mxn];
int fa[mxn][30],dep[mxn];
bool vis[mxn];
struct node {
int to,nxt;
}e[mxn<<1];
void add(int u,int v) {
++ecnt;
e[ecnt].to=v;
e[ecnt].nxt=head[u];
head[u]=ecnt;
++ecnt;
e[ecnt].to=u;
e[ecnt].nxt=head[v];
head[v]=ecnt;
}
void dfs(int u,int f) {
vis[u]=1;
for(int i=head[u],v;i;i=e[i].nxt) {
v=e[i].to;
if(vis[v]) continue;
dep[v]=dep[u]+1;
fa[v][0]=u;
dfs(v,u);
}
}
void fill () {
for(int i=1;i<=29;i++)
for(int j=1;j<=n;j++)
fa[j][i]=fa[fa[j][i-1]][i-1];
}
int lca(int x,int y) {
if(dep[x]<dep[y]) swap(x,y);
for(int i=29;i>=0;i--)
if(dep[fa[x][i]]>=dep[y]) x=fa[x][i];
if(x==y) return x;
for(int i=29;i>=0;i--) {
if(fa[x][i]!=fa[y][i]) {
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];
}
void sum(int u,int f) {
for(int i=head[u],v;i;i=e[i].nxt) {
v=e[i].to;
if(v==f) continue;
sum(v,u);
d[u]+=d[v];
}
}
int main(){
n=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1,x,y;i<n;i++) x=read(),y=read(),add(x,y);
dep[1]=1;
dfs(1,0);
fill();
for(int i=1;i<n;i++) {
int L=lca(a[i],a[i+1]);
d[a[i]]++;
d[a[i+1]]++;
d[L]--;
d[fa[L][0]]--;
}
sum(1,0);
for(int i=2;i<=n;i++) d[a[i]]--;
for(int i=1;i<=n;i++) printf("%d\n",d[i]);
return 0;
}
树上前缀和
定义为一个点到根路径的点权和
差分和数据结构的结合
对于一个支持单点修改、区间求和的数据结构,如果使用差分,就可以支持区间加法、单点查询(维护差分数组)
甚至可以支持区间加法、区间求和
一个经典的例子就是用树状数组来完成这些事情
用DFS序还可以把放到树上,区间变成子树
贪心
大胆猜想,无需证明
luoguP1376
第i周生产需要代价\(x=min\{c[i]*y[i],c[j]*y[i]+s*y[i]*(i-j)\} \ j\in [1,i-1]\)
那么对于第i周,我们贪心的选择最小的即可:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read() {
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-' ) ans=-ans;
return ans;
}
ll n,s;
ll c[10010],y[10010];
int main(){
n=read();s=read();
for(int i=1;i<=n;i++) {
c[i]=read();
y[i]=read();
}
ll minn;
ll ans=0;
for(int i=1;i<=n;i++) {
minn=y[i]*c[i];
for(int j=1;j<i;j++)
minn=min(minn,c[j]*y[i]+y[i]*s*(i-j));
ans+=minn;
}
printf("%lld",ans);
return 0;
}
排序不等式:
正序和不小于乱序和,乱序和不小于逆序和
A[i] B[i]
均从小到大排序,则:
\(\sum A[i]*B[j]=A数组从小到大排序*B数组从小到大排序(max) > \sum A[i]*B[j]A数组从大到小排序*B数组从小到大排序\)
洛谷P1842
只想大胆猜想,不想小心证明:
把W+S较小的放在上面
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read() {
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-' ) ans=-ans;
return ans;
}
const int mxn=50010;
ll n;
struct node {
ll s,w;
}cow[mxn];
bool cmp(node a,node b) {
return a.s+a.w<b.s+b.w;
}
int main(){
n=read();
for(int i=1;i<=n;i++) {
cow[i].w=read();
cow[i].s=read();
}
sort(cow+1,cow+n+1,cmp);
ll ans=-2147483647;
ll sum=0;
for(int i=1;i<=n;i++) {
ans=max(ans,sum-cow[i].s);
sum+=cow[i].w;
}
printf("%lld",ans);
return 0;
}
p1223√
p1012√
p1080 高精×
一个有点厉害的题
有一个初始为0的数X
有若干个操作,第i个操作是给X先加上Ai再减去Bi,这两个都是非负的数
找到一个操作的排列,使得进行完操作之后,X最大的时候最小
考虑一个完整的不被分割的序列,可以用一个二元组(sum, max)来表示。 sum是序列中的和, max是最大前缀和
那么每个操作单独看成的序列,就是\((A_i − B_i, A_i)\)
两个序列分别是\((suma, maxa)\)和\((sumb, maxb)\),如果把第二个序列接到第一个序列后面,新序列就是\((suma +sumb, max(maxa, suma + maxb))\)
按照前面的贪心方法,比较两个元素排序的关键字,就是两种方法新得到的max比大小
相等的时候显然把sum较小的放到前面对全局的影响更优
这个题还可以放到树上 ,具体而言就是对于操作的先后顺序有一些限制,而且限制关系形成了一颗树
哈夫曼编码
Kruskal求最小生成树
Dijkstra求单源最短路
调整法贪心
p1230
(没删调试信息wa好久)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read() {
ll ans=0;
char last=' ',ch=getchar();
while(ch>'9'||ch<'0') last=ch,ch=getchar();
while(ch>='0'&&ch<='9') ans=(ans<<1)+(ans<<3)+ch-'0',ch=getchar();
if(last=='-' ) ans=-ans;
return ans;
}
const int mxn=510;
bool vis[mxn];
int m,n;
struct node {
int ti,mon;
}a[mxn];
bool cmp(node x,node y) {
return x.mon>y.mon;
}
int main(){
m=read();
n=read();
for(int i=1;i<=n;i++) a[i].ti=read();
for(int i=1;i<=n;i++) a[i].mon=read();
sort(a+1,a+n+1,cmp);
bool bj=0;
int ans=0;
for(int i=1;i<=n;i++) {
bj=0;
for(int j=a[i].ti;j;j--) {
if(!vis[j]){
bj=1;
vis[j]=1;
break;
}
}
if(!bj) {
for(int j=n;j;j--) {
if(!vis[j]) {
vis[j]=1;
break;
}
}
ans+=a[i].mon;
}
}
int k=m-ans;
printf("%d\n",k);
}
p4053(gugugu)
考虑按照截止时间排序,能修就修
一个关于匹配的模型:
max>sum-max 无法匹配
否则一定存在构造:
按照a1……an依次排开:
a11,a12,a13,……,b11,b12………………n11,n12,……
i与i+sum/2匹配