在单调栈的运算过程中,当栈顶大于a[i]时,栈顶不断退栈。直到栈定值小于等于a[i]或者栈顶为空时, 在将a[i]存入到栈中。 并将当前栈中的值的个数存入到b[i]中(a[i]下标 i 与b[i]下标i相同)。
现在有一组关于n的排列(1~n的组合)组成a[],但我们不知道,但是会给出k个b[]的值。问能否通过这k个b[]的值算出a[]的排列(答案可以有多个,其中一个满足条件的即可。)。如果没有这样的a[]的排列的话输出-1。
输入:
第一行输入 n ,k;
后面k行每行输入两个值,输入b[idx] 的idx , 以及b[idx]对应的值。
输出:
如果能够有这么一种排列,输出a[]的排列。
如果不能够有这么一种排列,则输出-1.
首先我们根据给出的b[],来进行单调栈的处理,如果b[i]没有被赋值,则可以将i插入。因为我们只要当遍历到b[j]这个有输入时,单调栈中的元素个数t >= b[j ] - 1,就可以满足构造出a[],所以b[i]未输入值插入i入栈中,是成立的。然后判断栈中元素t 是否 >= b[j] - 1,如果不成立,则输出-1. 否则的话,根据栈中元素t和 b[j] 的关系,去将栈中元素退栈,直到t == b[j] - 1. 而退栈的那些元素的含义就是a[退栈元素] > a[j]. 就连一条 退栈元素值 -> j的一条边,(也就是构造一个大值 -> 小值的边),最后当无法退栈后, 说明a[栈顶元素] <= a[j]. 然后连一条j ->栈顶元素的边。(构造一个大值 - >小值的边)。
最后得到一个拓扑图。因为我们一直通过大值 -> 小值, 不可能成环,有向无环图一定是拓扑图,然后求一遍拓扑序。 而这个拓扑序,用n,n-1,n-2不断的赋值(因为入度为0,即为当前最大值)。即可a[].
由于我们只知道部分b[],所以通过单调栈的方式将对应的各个a[]之间的关系表示出来。
首先需要知道,单调栈中维护的是什么。 此时我们单调栈中存入的下标,维护着单调栈中的元素个数 与 b[]之间的关系。
情况1:如果此刻的下标i,对应的b[i]并未给出,那么实际上我们可以将对应的i加入到单调栈中。因为我们只要维护当到达存在输入值的b[j]的时候,单调栈中的元素 + 1 >= b[j]即可。因为满足这个条件则必定能够组成一个成立的a[]。
所以栈中元素多了,没有关系。一样能够构造出对应的a[]. 但是如果栈中元素少了,则就无法构造出对应的a[]. 于是我们将下标i加入到单调栈中。(而这个加入到单调栈中实际上是有含义的,含义就是a[栈顶]要大于其他a[栈内元素]的含义)。
为什么当单调栈中元素 + 1 < b[j]时不满足呢? 因为b[j] 的最大值为 b[j - 1] + 1. 所以b[j]不会比当前单调栈中元素多1以上。这是无法实现的。
情况2:当前下标i对应的b[i]输入了值, 如果b[i] 大于 栈中元素个数 + 1则无法构造出a[].
情况3:当前下标i对应的b[i]输入了值,如果b[i]小于 栈中元素个数 + 1,则将栈顶元素不断退栈。(相当于存入的a[栈顶元素] > a[i]),直到b[i] == 栈中元素个数 + 1.
我们所建的图的每一条边的含义都是 大值 指向 小值。( 大值 -> 小值)。
在上面的情况3中,栈顶不断退栈,所以对应的a[栈顶值] 大于 a[i]. 所以将 栈顶值 与 i之间连一条边,表示a[栈顶值](大值) -> a[i].
最后栈中留下的值,与a[i]的关系就是,a[栈顶值] 小于 a[i]. 所以a[i] - > a[栈顶值]。
按照这两种情况进行建图。
是否一定要对此两种情况都进行建边表示关系呢?是否可以只对一种情况进行建边关系呢?
实际上是不行的。必须对两种情况都进行建图。
举例:
最小值a,次大值b,最大值c, 次小值d。
按照顺序,依次进栈
最小值a,次大值b,最大值c,
并按照此种关系进行建图,
c ->b , b -> a.
然后次小值进入,
栈中变为,a,d
c->d,b->d,d->a
形成了一个拓扑图。正确。
那么如果只对一种情况建图,如退栈时才建图的话呢?
b,c,退栈,则b -> d, c->d.
可以很明显的看出图是不完整的。
如果只对剩余栈中未退栈元素 和 当前a[i] 进行建图的话呢?
举例:
最大a,次大b,小c,最小d
a进栈,
a退栈,b进栈
b退栈,c进栈
c退栈,d进栈
则可以发现,无边可以建立。
所以,两种情况都需要进行建图创边。最后得到一个拓扑图。
为什么一定是拓扑图呢?因为其为有向图。 并且一定无环。
边的含义就是大值 指向 小值。
如果有环的话, 大值1 -> 小值1 , 小值1 -> 大值1. 就矛盾了。
所以按照此种建图,必定不会有环,又是有向图。 所以必定为拓扑图。
那么拓扑图,就必定有入度为0. 而入度为0,就是最大值,赋值为n.
然后通过这个点不断进行求解拓扑序列的过程中,不断赋值,n - 1, n -2 , n -3 ......1。
还需要注意一点:
所以建边的时候要注意需要多建些边。
# include
# include
# include
using namespace std;
const int N = 1e6 + 10 , M = 2 * N;
int st[N],tt = -1; // 单调栈
int a[N];
int b[N];
int n,k;
int h[N],e[M],ne[M],ru[N],idx; // 同时需要注意一点,建图的时候边的个数可能会大于N。
queue q;
//所连的边为 大值 指向 小值
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void top()
{
int temp = n;
for(int i = 1 ; i <= n ; i++)
{
if(ru[i] == 0)
{
q.push(i);
}
}
while(q.size())
{
int t = q.front();
q.pop();
a[t] = temp--;
for(int i = h[t] ; i != -1; i = ne[i])
{
int j = e[i];
ru[j]--;
if(ru[j] == 0)
{
q.push(j);
}
}
}
}
int main()
{
memset(h,-1,sizeof h);
scanf("%d %d",&n,&k);
while(k--)
{
int x;
scanf("%d",&x);
scanf("%d",&b[x]);
}
int res = 0; // 当前栈中的元素个数
for(int i = 1 ; i <= n ; i++)
{
if(b[i])
{
if(b[i] > res + 1) // b[i]不可能大于当前栈中元素个数 + 1
{
printf("-1\n");
return 0;
}
while(res != 0 && b[i] < res + 1) //如果当前栈中元素个数多于b[i]所要的,则说明栈中有部分元素应该退栈,即对应值比a[i]大
{
// 栈中值大于 a[i].
add(st[tt],i); // 栈中值指向a[i]
ru[i]++; // 入读增加
//printf("----%d---->%d %d---\n",st[tt],i,ru[i]);
tt--;
res--;
} //直到b[i] == res + 1.
}
if(tt != -1)
{
add(i,st[tt]); //栈中的值小于a[i]
ru[ st[tt] ]++; // 入度增加
// printf("----%d---->%d %d---\n",i,st[tt],ru[st[tt]]);
}
st[++tt] = i; //将i存入到栈中,
res++; // 栈中元素增加了一个
}
top();
for(int i = 1 ; i <= n ; i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
首先将b[]的值我们进行初始化赋值,因为b[i] = temp, 则b[1~i-1]必定至少出现一次1~temp的值,所以我们让这些值相邻,b[i- 1] = b[i] - 1, b[i -2] = b[i - 1] - 1, 同时注意,可能这时候,b[]会被赋为负值,但是最小也为1. 所以b[i - 1] = max(1,b[i] - 1), b[i - 2] = max(1,b[i - 1] - 1)以此类推。
同时b[]可能最右边没有赋值,那么给其b[]全部赋值为1即可。因为我们能够构造出a[]的原因就是b[i] - b[i - 1] <= 1,且b[1] 等于 1 即可。
最后将b[i]的值和下标i存入到pair
数组中, 对其进行小值在前,小值相同则下标更大的在前的方式进行排序。 因为b[最右边某个下标] = 1, 则说明这个值必定为1。
因为1的存入,会将左边所有值都给清空,只留下1,同时右边无法清掉1。所以b[]最右边的1,一定存1.
1存入后, 在存入2.则右边第二个1,或者右边第一个2存入的就是2这个值。
如: 1 1
或者1 2
或者2 1的情况
以此类推,按照排好的顺序,对应的下标a[i] = 1,2,3,4,5,6,7,这样按顺序赋值即可。
首先,先让我们想一下输出-1的情况是什么样的呢?
1. 如果b[idx]的值 > idx 的话,一定不满足条件。
因为在第idx个,b[idx]最多只能有idx个。
2.如果两个相邻的b[],如果b[i + 1] - b[i]大于1,则不可能。
因为当到下标i的时候,栈中最多只能由b[i]个,那么到i + 1下标的时候,就算栈中一个没有退,在加上自己a[i + 1]也最多为b[i] + 1个。 所以b[i + 1] - b[i] 不能 > 1.
但是b[i + 1]可以小于b[i]. 因为比如a[i + 1]为1,则前面的全部退栈,b[i + 1] = 1.
同时我们还应该要知道,如果给了一个b[i] == temp的话,那么说明,下标i的前面, b[1 ~ i - 1](下标从1开始)一定存在 1 ~ temp - 1所有至少出现一次。因为 temp的存在,必定至少经过了一次从temp - 1通过 + 1得到temp.
当然,实际上也可能会是从 大于temp 通过退栈 到达栈中temp个。 但是如上面标红所说,至少要经过一次从temp - 1经过 + 1到达temp, 所以下标b[1 ~ i - 1]中必定存在1 ~ temp - 1的所有值。
那么我们知道的为b[i],而题目又是只要满足条件的一个,所以不妨让b[i - 1] = b[i] - 1, b[i - 2] = b[i - 1] - 1,以此类推。一直到b[j]存在 或者 b[1].
而这就有4种需要考虑到的情况了:
1.b[i - 1] = b[i] - 1的话,满足不输出 -1 的 一个条件,那就是b[i] - b[i - 1] 不 > 1。
2.那么另一个条件呢? 如果b[idx] > idx是否要判断呢?
实际上我们可以通过另一种方式进行判断,
因为存在前面b[j]通过了这种方式进行了初始化(因为存在多个已知的b[]),所以当我们通过此种方式到达了b[j + 1]后停下了, 那么判断b[j + 1] - b[j] 是否 > 1即可。
或者就是b[j]还未存在,现在初始化的是第一个b[],那么就到b[1] 是否等于1,因为b[1]必定为1,如果b[1]都不等于1,那么必定b[idx] > idx了。
3.情况3就是如果b[]的下标很大。
举例:比如现在已知,b[100] = 5;
则第b[99] =4 , b[98] = 3, b[97] = 2, b[96] = 1,
注意:这时b[95~1]呢?该怎么赋值?
难到赋值为负数吗?肯定不可能。
那么应该怎么赋值呢?
赋值为1就可以了(当然实际上可以赋很多值,只是1必定正确)。
因为赋值为1,如果到了b[1] 必定为1,没得问题。
同时,b[j] - b[j - 1]必定小于1,因为b[j] == 1了,b[j - 1]不管是1,还是原先已经赋值,得到的结果必定小于1.
所以赋值为1即可,但是只要满足这两个条件的值,实际上都可以。只是1是最方便的,不用判断条件,
4.同时还要注意一个,我们通过b[i]然后赋值b[1~i-1],那么问题来了。
如果给出的b[idx]的idx最大的情况为10, 而我们给的n有100呢?
就会发现b[idx + 1 ~ 100]就还没有赋值,
而我们现在的思想是先赋值完所有的b[]。至于为什么先赋值完所有的b[],后面会说。
而这时候,和上面的情况3一样,我们把后面的值全部赋值为1即可。因为赋值为1的话,必定满足能够组成a[]的条件。当然其他的满足条件的值也可以。
比如b[idx + 1] = b[idx] + 1, b[idx + 2] = b[idx + 1] + 1.....
或者b[idx + 1] = b[idx] , b[idx + 2] = b[idx + 1]....都是满足条件的。
只是1最为方便,最简单,并且必定满足构成a[]的条件所以使用1而已。
因为我们会发现这样一个方式。 首先我们先找到b[]的值为1的情况。
找到所有b[]为1的情况要干什么? 找到b[]中最右边的一个1,也就是下标最大的b[idx] = 1.
那么那个a[idx] 必定为1.
为什么?
证明:
如果在a[idx]的前面放的是1的话,由于我们a[]是1~n的排列.
所以可以知道,1放在idx的前面,那么1永远不会退栈,那么b[idx] 则至少为2不可能为1.矛盾了。
如果a[idx]的后面放的是1的话,比如a[idx + 10] 放的是1,那么b[idx + 10]必定为1,因为栈中所有值都退栈了,最后将a[idx + 10]放入,所以b[idx + 10]才为1,而我们找到了b[idx]为b[]中为1的最右边的情况, 那么就矛盾了。
所以可以知道,b[]最右边的1,那么a[]在这个位置必定为1。
同样的,b[]最右边的值为2,那么这个值必定为2,如果b[]最右边的1在它后面,则1被抢了,那么它的前面就不能放1,就放3. 如果b[]最右边的1在它前面,因为我们构造的时候就是这样,b[idx] = 2 , 则我们构造b[idx - 1] = b[idx] - 1, 也就是1了。 那么a[idx - 1]为1,a[idx] 为2.
满足条件。
以此类推。
所以我们先构造好所有的b[],然后根据构造好的b[],存入到pair
[]的数组中,对这个数组进行 值最小排序,如果值相同是,最右边的值放前面。 如1 1
则第一个1对应的值a[]赋值2, 第二个1对应的值a[]赋值为1.
那么是否会存在 b[]中没有1的情况呢?是不可能存在的。
因为至少会存在b[1]为1,那么至少就会使得a[1] 为1, 也就是1一定会被用掉,然后去用2.
而按照我们创建b[]的方式可以知道,只要初始给值的时候,给出一个b[] = temp, 那么就至少会存在1~temp的值给b[],也就至少会用掉所有的1~temp.
以此类推, 比如给出的b[r(输入时给出的最大的下标)] = temp,
那么从b[r + 1] ... b[n]都为1, 则从右往左就用掉了1,2,3,..... n - r . 然后前面的根据b[]的大小,赋值n - r~r。
# include
# include
using namespace std;
const int N = 1e6 + 10;
int a[N];
int b[N];
pair p[N];
int n,k;
bool cmp(pair a , pair b)
{
if(a.first == b.first)
{
return a.second > b.second; // 如果值相同,则在右边的先赋值
}
return a.first < b.first; // 值小的在前面完成。
}
int main()
{
scanf("%d %d",&n,&k);
while(k--)
{
int idx , v;
scanf("%d %d",&idx,&v);
b[idx] = v;
}
for(int i = 1 ; i <= n ; i++)
{
if(b[i])
{
int j = i - 1;
while(j)
{
if(b[j]) // 如果前面已经将b[j]赋值了,就可以跳出循环了
{
break;
}
b[j] = max(1,b[j + 1] - 1); // 因为这样的话b[]可能为0或负数,但是b[]实际上最小为1
j--;
}
}
}
for(int i = 1 ; i <= n ; i++) // 因为我们是通过b[i]然后向左进行初始化的,可能存在部分后面的值没有进行初始化,而后面的初始化则只要后面的值 比 前面的值 不会大于1就可以了。比如全部赋值为1等等都是可以的。
{
if(!b[i])
{
b[i] = 1;
}
}
// 而无法构建出的情况就是 b[j] - b[j - 1] > 1 就构建不出 或者 b[1]不等于1,也就是第一个b[i] 的值 > i
if(b[1] != 1)
{
printf("-1\n");
return 0;
}
for(int i = 2 ; i <= n ; i++)
{
if(b[i] - b[i - 1] > 1)
{
printf("-1\n");
return 0;
}
}
// 然后就是将b[]中的值存入到pair 中,因为我们会将值进行排序
//排序方式:1.小值在前, 2.如果值相同,b[]的下标在右边的在前, 因为我们会先把1的值完成,最右边的1一定为1,
for(int i = 1 ; i <= n ; i++)
{
p[i].first = b[i];
p[i].second = i;
}
sort(p + 1, p + 1 + n , cmp);
for(int i = 1 ; i <= n ; i++)
{
a[p[i].second] = i;
}
for(int i = 1 ; i <= n ; i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}
方式2的解法只适用于,当b[i - 1] - b[i] >= -1的情况下才能够适用。
因为这道题目是单调栈的缘故,所以满足b[i - 1] - b[i] >= -1的情况,所以才能够使用这种,直接顺序赋值的情况。
有一个思路类似,但不能够直接赋值的题目:
https://www.acwing.com/problem/content/245/
此题为树状数组的一道题目,此题就不能够直接赋值。 虽然其思想也依然是最右边的最小值一定为1,但是其不能够顺序赋值,应为它不满足b[i - 1] - b[i] >= -1的条件。它的所有a[]不会退栈。而是找i前面小于a[i]所有值。
为什么必须要满足b[i - 1] - b[i] >= -1的条件才能用这种方式呢?
因为这样才能够满足前面进行的赋值一定大于后面进行的赋值,按照我们赋值的方式,b[]越大的,赋值越大, 而b[i - 1] 在b[i]的前面,比b[i]大,所以导致前面的赋值永远比后面的赋值要大。导致前面赋的值不会影响到后面的b[i].
而我们后面给出的这道树状数组的题目,则b[]的值跨度很大,出现前面b[i - 1] 要小于b[i]的情况,
所以这道题目,你会发现,所有赋值为b[i - 1] - b[i] >= -1下的赋值,用这个方法都是成立的。
但是给出b[i - 1]小于 b[i]的情况时,这个方法就无法成立,只能出现WA了。
比如b[23] = 0 , b[1] = 0 , b[22] = 1 , b[47] = 2;
那么a[23] = 1,a[1] = 2 , a[22] = 3, a[47] 的构造是不可能出现b[47] == 2的情况的。
综上:方式2的解法,只适用于这种特殊的题目,单调栈下使得维护的b[]为b[i - 1] - b[i] >= -1的情况下才能用此解法(顺序赋值)。
使用树状数组的方式,在方式2的基础上 来求解这道题目。(因为树状数组的方式 要比 方式2的顺序赋值更加的通用)
参考题目链接(谜一样的牛):https://www.acwing.com/problem/content/description/245/
参考题目题解:https://blog.csdn.net/qq_49120553/article/details/119004022
前面还是一样。算出所有的b[i].
举例来说:
b[]为: 1 2 3 2 1
那么我们从后往前进行计算, b[5] == 1, 所以可以知道,前面没有比它更小的值了,所以a[5] == 1. (取值从原来的1,2,3,4,5 变为了 2,3,4,5)。
而b[4] == 2, 所以1~3里面有一个值比它小, 所以我们取剩下值中,倒数第二小的值,3.
所以a[4] == 3. (取值从原来的2,3,4,5变为了2,4,5)
而b[3] == 3 , 所以1~2里面有两个值比它小,所以我们取剩下的值中,倒数第三个小的值,5
所以a[3] == 5,(取值从原来的2,4,5,变为了 2,4)
而b[2] == 2, 所以1里面有一个值比它小,所以我们取剩下的中,倒数第二个小的,4
所以a[2] == 4.(取值从原来的2,4 变为了2)
所以a[1] == 2.
这样的方式求解。与方式2类似,但是又不尽相同,如上面所说,方式2的顺序赋值要求比现在的方式3要严格。需要b[i - 1] - b[i] >= -1才行。
那么问题来了。如何实现方式3呢?
我们可以使用树状数组的方式来维护我们现在还能够用的数值。
由于树状数组维护的是前缀和。 所以我们树状数组中的前缀和的含义就是 1~x中还有多少个数。
每一个数在树状数组中刚开始的时候都是1,
每使用掉一个值,那么树状数组中这个值就被-1. 使得前缀和 -1.
举例:我们要找当前剩下的数中第 k 小的数。
那么就可以使用二分的方式进行查询。
寻找树状数组中第一个 1~x区间 其和 大于等于 k。(也就是1~x中还剩下大于等于k个值,而又是第一个大于等于k的,其实就是第一个和为k的情况, 那么x的含义不就是当前1~n中剩下的第k小的数了嘛。)
所以可以通过树状数组 加 二分 的方式 实现。
思路三的由来完全是因为我这次刚好做到这个"谜一样的牛"这道题目,感觉与这道题目类似,于是使用了一下方式2求解那道题目,结果发现使用方式2的顺序赋值的方式无法求解出那道题目。
但是通过自己举的一些思路,发现方式2解题方式,可以解掉那些b[i -1] - b[i] >= -1的情况,
所以自行想了一下为什么b[i - 1] - b[i] >= -1才能使用方式2. 以及这两道题目的差别。
但未必正确。只是我个人感觉方式2的顺序赋值需要这个条件(b[i - 1] - b[i] >= -1),而方式3的树状数组的方式感觉更为通用而已 (对b[i]无条件限制)。
// 使用树状数组的方式求解
# include
# include
using namespace std;
const int N = 1e6 + 10;
int a[N];
int b[N];
int c[N];
int n,k;
int lowbit(int x)
{
return x & -x;
}
void add(int x , int v)
{
for(int i = x ; i <= n ; i += lowbit(i))
{
c[i] += v;
}
}
int sum(int x)
{
int res = 0;
for(int i = x ; i > 0 ; i -= lowbit(i))
{
res += c[i];
}
return res;
}
int main()
{
scanf("%d %d",&n,&k);
while(k--)
{
int idx , v;
scanf("%d %d",&idx,&v);
b[idx] = v;
}
for(int i = 1 ; i <= n ; i++)
{
if(b[i])
{
int j = i - 1;
while(j)
{
if(b[j]) // 如果前面已经将b[j]赋值了,就可以跳出循环了
{
break;
}
b[j] = max(1,b[j + 1] - 1); // 因为这样的话b[]可能为0或负数,但是b[]实际上最小为1
j--;
}
}
}
for(int i = 1 ; i <= n ; i++) // 因为我们是通过b[i]然后向左进行初始化的,可能存在部分后面的值没有进行初始化,而后面的初始化则只要后面的值 比 前面的值 不会大于1就可以了。比如全部赋值为1等等都是可以的。
{
if(!b[i])
{
b[i] = 1;
}
}
// 而无法构建出的情况就是 b[j] - b[j - 1] > 1 就构建不出 或者 b[1]不等于1,也就是第一个b[i] 的值 > i
if(b[1] != 1)
{
printf("-1\n");
return 0;
}
for(int i = 2 ; i <= n ; i++)
{
if(b[i] - b[i - 1] > 1)
{
printf("-1\n");
return 0;
}
}
for(int i = 1 ; i <= n ; i++)
{
add(i,1); // 每个i值都可以用一次
}
for(int i = n ; i >= 1 ; i--)
{
int t = b[i]; // i的前面有h[i]个比它小的,所以它是当前还剩下值的第h[i] + 1小的值
int l = 1 , r = n;
while(l < r) // 找第一个大于等于t的值
{
int mid = (l + r) / 2;
if(sum(mid) >= t)
{
r = mid;
}
else
{
l = mid + 1;
}
}
a[i] = l;
add(l,-1); // l被用掉了。所以减1.
}
for(int i = 1 ; i <= n ; i++)
{
printf("%d ",a[i]);
}
printf("\n");
return 0;
}