丢失的牛综合讲解Lost Cow

1~n,乱序排列,告诉每个位置的前面的数字中比它小的数的个数,求每个位置的数字是多少
Sample Input
5 //五头牛
1 //对于第2头牛来说,前面有1头比它小
2
1
0
Sample Output
2
4
5
3
1

Sol:查找第a[i]+1小的数字,可以权值线段树或树状数组.

下面这个是暴力程序,注意我们要选择的那个位置必须是“1”,代表尚未使用过的。。这是整个全篇所有程序的要点。。。O(N^2)

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int a[8010],f[8010],ans[8010];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=2;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=n;i>=1;i--)
    {
        int sum=0;
        for(int j=1;j<=n;j++)
        {
            if(!f[j])sum++;
            if(sum==a[i]+1)
            {
                ans[i]=j;
                f[j]=1;
                break;  
//找到第一个满足条件的位置就退出. } } } for(int i=1;i<=n;i++) printf("%d\n",ans[i]); return 0; }

  

链表稍微优化了点,但仍是O(N^2)

/*
Sample Input
5 //五头牛
1 //对于第2头牛来说,前面有1头比它小
2 
1 
0 
Sample Output
2 
4 
5 
3 
1
以这个样例来说
建立链表
0 1 2 3 4 5
对于读入的1 2 1 0
先处理最右边的0
代表从链表中,从0开始出发,走0+1步,于是走到1
然后删了它得到0 2 3 4 5
再处理1
代表从链表中,从0开始出发,走1+1=2步,于是走到3
................

*/ 


#include<stdio.h>
int after[10000];
int t[10000],ans[10000];
int ss(int key)
{
	int temp=0,pre;
	for(int i=0;i<=key;i++)
	//从0开始向后走key+1步。 
	{
		pre=temp;
		temp=after[temp];
	}
	//形成这样的pre....temp....
	after[pre]=after[temp];
	return temp;
}
int main()
{
	int n,i;
	scanf("%d",&n);
	for(i=0;i<=n;i++)
	    after[i]=i+1;
	//建立一个向后的指针 
	for(i=2;i<=n;i++)
	    scanf("%d",&t[i]);
	for(i=n;i>=2;i--)
	     ans[i]=ss(t[i]);
	ans[1]=after[0];
	for(i=1;i<=n;i++)
	printf("%d\n",ans[i]);
}

  

 

 

 

 

下面是树状数组,二分位置的。N*Log2N*Log2N

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int a[10000],c[10000],h[10000],n;

int ask(int x)
{
	int ans=0;
	for(;x;x-=x&-x) ans+=c[x];
	return ans;
}

void add(int x,int y)
{
	for(;x<=n;x+=x&-x) c[x]+=y;
}

int query(int x)
{
	int l=1,r=n,loc=n;
	while(l<=r) //二分查找位置
	{
		int mid=(l+r)/2;
		if(ask(mid)==x) 
		   {
		    	if (mid<loc)
			        loc=mid;
			    r=mid-1;
					
			}
		else
	     	if (ask(mid)<x)
	            l=mid+1; 
	        else 
		         r=mid-1;
	}
//	cout<<x<<"          "<<loc<<endl;
	return loc;
}

int main()
{
	cin>>n;
	for(int i=2;i<=n;i++) scanf("%d",&a[i]);
//	a[1]=0;
	memset(c,0,sizeof(c));
	for(int i=1;i<=n;i++) add(i,1);
	for(int i=n;i;i--)
	{
	    h[i]=query(a[i]+1);
	    add(h[i],-1);
	}
	for(int i=1;i<=n;i++) cout<<h[i]<<endl;
	//system("pause");
}

 

 下面是用一种倍增的算法

最简单的题是这个...

P02612. 数组的二分查找4 

 

# Description

给出一个长度为 N的不下降序列, m次询问数字x在序列中的哪个位置,如果有多个解输出位置最靠左边的,无解输出-1

 


# Format

## Input
第一行给出数字N,M

第二行N个数字

第三行M个数字,其值<=1e9

N<=1e6

M<=1e5


## Output
一行输出M个数字,代表结果
每个数字后面有个空格

# Samples

```input1
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
```

```output1
1 2 -1
```

 Sol:

认为从0点开始跳,所跳距离为8,4,2,1这样的长度

因为所跳的距离必能分解成一个2进制数字。

但在跳的时候应该满足所跳的位置上的数字严格小于所要找的数字。

因为在跳的时候,前期可能因为跳得距离比较长,一下子跳过头了。

于是采用逼近的策略,保证所跳的位置上的数字严格小于所要找的数字。

最终检测下所跳的点的右边是不是要找的数字即可。。

 

这个过程与查找Lca是非常相似的,查找Lca也是一直跳啊跳

最后所停止位置的父亲点即是最终结果。。

 

 

 

 

例如我们要在

1 3 3 5 7 7 9 9 9 12中找7第一次出现的位置

则最开始找到8这个位置,发现其值为9>7

再找到第4个位置,其值为5,是可以的

然后尝试再右移2位,发现值正好为7,不满足小于的条件,不能移动

再尝试右移1位,发现值正好也为7,不能移动

  于是退出后,检查下第5+1这个位置的数字是不是7,如果是则找到结果

否则没有找到.

 

代码如下:

#include<cstdio>
using namespace std;
int n,m,a[2000005];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	 	scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
	{
		int x,ans=0;
		scanf("%d",&x); 
		for(int j=20;j>=0;j--)//2的20次方已经超过10的6次方
		 	if(a[ans+(1<<j)]<x&&ans+(1<<j)<=n)
		  		ans+=(1<<j);
		if(a[ans+1]!=x)printf("-1 ");//输出
		else printf("%d ",ans+1);
	}
	return 0;
}

  

 

2022.3.15

有了上面这个基础操作,在本题中查找第一个值为a[i]+1的位置就简单多了。。。。

 

 

 

 

 

不用二分的话,直接在Bit中找第K小的,并且位置要尽量靠左时,

不能直接去找,而是应该找第 K-1小的,然后位置再后移动1位。

因为在BIT中是先加大区间,再加小区间,例如我们对1100这一段,我们要找到前缀和为2的,其实前2个数字的前缀和就为2了

但在BIT中会先加区间为4的。

 

 

 

 

  假设给定数列如下:1111111100110011

STEP 1:保证找到一个位置,其前缀为a[i],并且这个位置越靠后越好,也就是说再多1位,总和就超过a[i]了,如果这个位置靠前的话,则后面一位可能是0,再多加1位,也不能超过a[i]。

为什么不能跟从前一样直接找第一个位置,其前缀和为a[i]+1呢?

因为我们加的区间是从大到小,例如0010,前四个数字之和为1,前3个数字之和也为1.

用现在这种倍增的加法,无法保证找到最靠前的位置。


设a[i]=10
此时我们找的ans是最靠后一个位置,其前缀的为10
此时会找到倒数第3个位置。
程序是这样做到的
先试图去加c[16]=12,发现不能加
先试图去加c[8]=8,发现能加,就加上
再试图去加第8个位置后面连续4个数字即c[12]=2,发现能加,就加上
再试图去加第12个位置后面连续2个数字,c[14]=0,发现能加,也加上
再加图去加第14个位置后面连续1个数字,c[15]=1,发现不能加了,
于是第14位就是我们要的,其数字和为10.

整个过程跟求Lca倍增法非常类似

如果是从前二分找位置的话,找的是某个位置其前缀和为11.
这个位置是第一个前缀和为11的,即倒数第2个位置


于是我们此时找到的结果要加1

 

这个条件ans + (1<<p) <=n
STEP 2:
是保证要加的数字个数<=n,例如n=14时
我们让其先加8个,再加4个,2个,1个。这样是可以加过头的

 

如果没有这个限制条件,也有可能N=16

我们一开始就加了16,然后再加的位置就是C[24]了,这个位置根本是不存在的。

#include<bits/stdc++.h>
using namespace std;

inline void read(int &x) {
	int k=0;
	char f=1;
	char c=getchar();
	for(; !isdigit(c);
	        c=getchar())
		if(c=='-')
			f=-1;
	for(; isdigit(c);
	        c=getchar())
		k=k*10+c-'0';
	x=k*f;
}

const int maxn=1e5+34;

int n;
int a[maxn],b[maxn],c[maxn*2],h[maxn];

int lowbit(int x) {
	return x&-x;
}
int ask(int x) {
	int ans=0;
	for(; x; x-=lowbit(x))ans+=c[x];
	return ans;
}
void add(int x,int y) {
	for(; x<=n; x+=lowbit(x))c[x]+=y;
}

int main() {
	scanf("%d",&n);
	for(int i=2; i<=n; i++) {
		scanf("%d",&a[i]);
	}
	for(int i=1; i<=n; i++) {
		b[i]=1;
		add(i,1);
	}


	int lim=(int)log2(n);
	for(int i=n; i>=1; i--) {
		int ans=0,sum=0;

		for(int p=lim; p>=0; p--) {
			if(ans + (1<<p) <=n && sum+c[ans+(1<<p)]<=a[i])
				//不能直接查找<=a[i]+1的位置,这样找出来的结果不准确
				//因为有些段的数字为0,其sum和为0,于是位置就右移了.
				//ans+1<<p代表要加哪一段的数字
				//当然写成 sum+c[ans+(1<<p)]<a[i]+1便与上面的程序统一了

			{
				sum+=c[ans+(1<<p)];
				ans+=(1<<p);
			}
		}
		h[i]=ans+1;
		add(ans+1,-1);
	}
	for(int i=1; i<=n; i++)printf("%d\n",h[i]);

}

  

 

 

 

  

 

  

 

  权值线段树,N*Log2N

#include<bits/stdc++.h>
using namespace std;
int n,a[8010],ans[8010];
int sum[80010];
void insert(int p,int l,int r,int x,int val) 
{
if(l==r) 
{
    sum[p]+=val;
    return;
}
int mid=(l+r)/2;
if(x<=mid) 
   insert(p*2,l,mid,x,val);
else 
    insert(p*2+1,mid+1,r,x,val);
sum[p]=sum[p*2]+sum[p*2+1];
}
int kth(int p,int l,int r,int x) 
{
if(l==r) return l;
int mid=(l+r)/2;
if(sum[p*2]<x) 
   return kth(p*2+1,mid+1,r,x-sum[p*2]);
else 
   return kth(p*2,l,mid,x);
}
int main() 
{
scanf("%d",&n);
for(int i=2;i<=n;i++) 
    scanf("%d",&a[i]);
for(int i=1;i<=n;i++) 
   a[i]++;
for(int i=1;i<=n;i++) 
    insert(1,1,n,i,1);
for(int i=n;i>=1;i--) 
{
ans[i]=kth(1,1,n,a[i]);
insert(1,1,n,ans[i],-1);
}
for(int i=1;i<=n;i++) 
    printf("%d\n",ans[i]);
}

  

 这个程序,更好看一点吧。

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 8e3 + 10;
int tree[maxn*8], p[maxn], res[maxn];

void build(int l, int r, int dex)
{
	if(l == r) tree[dex] = 1;
	else{
		int mid = (l+r)>>1;
		build(l, mid, dex*2), build(mid+1, r, dex*2+1);
		tree[dex] = tree[dex*2] + tree[dex*2+1];
	}
}
int query(int l, int r, int dex, int k)
{
	if(l == r) return l;
	int mid = (l+r)>>1;
	if(k <= tree[dex*2]) return query(l, mid, dex*2, k);
	else return query(mid+1, r, dex*2+1, k - tree[dex*2]);
 } 
void update(int l, int r, int dex, int x)
{
	if(l <= x && r >= x)
	{
		tree[dex]--;  //这个区间的数字个数要减少一个
		if(l != r)
		{
			int mid = (l+r)>>1;
			update(l, mid, dex*2, x), update(mid+1, r, dex*2+1, x);
		}
	}
 } 

int main()
{
	int n;
	while(scanf("%d", &n) != EOF){
		for(int i = 2; i <= n; i++) scanf("%d", &p[i]);
		p[0] = 0;
		build(1, n, 1);
		for(int i = n; i; i--){
			res[i] = query(1, n, 1, p[i]+1);
			update(1, n, 1, res[i]);
		}
		for(int i = 1; i <= n; i++) printf("%d\n", res[i]);		
	}
}

  

posted @ 2020-07-17 15:43  我微笑不代表我快乐  阅读(41)  评论(0编辑  收藏  举报