算法
(二分图,栈,染色法,贪心) $O(n^2)$
如果只有一个栈,则整个操作顺序是固定的:
- 从前往后遍历每个数,每次先将当前数压入栈中,如果后面的所有数均比栈顶元素大,则将栈顶弹出,否则栈顶不能被弹出。
因此,我们只需考虑将每个数分配给哪个栈即可。
这里有个很强的性质:
两个数 $i, j(i \le j)$ 不能被放入同一个栈中,当且仅当存在 $k, k > j$, 且 $q[k] < q[i] < q[j]$。
证明:
必要性:
如果有 i < j < k, 且 q[k] < q[i] < q[j],则因为 q[i] 和 q[j] 的后面均存在一个更小的q[k],因此q[i]和q[j]都不能从栈中被弹出,所以从栈底到栈顶的元素就不是单调的降序了,那么弹出时得到的序列就会出现逆序对。因此q[i]和q[j]不能被分到同一个栈中。
充分性:
如果q[i]和q[j]不满足上述条件,则我们在操作过程中一定不会出现矛盾。
在操作过程中,如果当前要入栈的数是q[j],那么此时:
- 所有大于q[j]的元素,都一定在栈中;
- 所有小于q[j]的元素,比如q[i] < q[j],则由于后面不存在q[k] < q[i],因此q[i]一定已经出栈;
所以此时将q[j]压入栈中后,从栈底到栈顶仍然可以保持降序,因此整个进栈、出栈过程是可以顺利进行的。
有了上述性质后,我们只需将所有满足条件的点分到两个栈中去即可。这一步可以转化成图论问题:
- 如果i, j满足条件,则在i和j之间连一条边。
然后判断是否是二分图即可。判断二分图可以用染色法,可以参考AcWing 860. 染色法判定二分图。
输出字典序最小的方案
答案要求字典序最小,因此从前往后染色时,需要优先将当前点分配到第一个栈中。
另外对于每一个栈,操作序列是唯一确定的,也就是'ab'
的相对顺序是固定的,且'cd'
的相对顺序也是固定的。
另外在最优解中,每个数插入哪个栈中也是唯一确定的,所以'ac'
的相对顺序也固定。
最后,输出序列是唯一确定的,所以'bd'
的相对顺序也固定。
所以在最终方案中,只有'ad'
、'bc'
的相对顺序可变,所以我们只需先随便求一个方案,然后将所有连续的bc
区间排序,以及连续的ad
区间排序即可。
时间复杂度
建图时需要枚举所有点对,时间复杂度是 $O(n^2)$。
染色法判定二分图的时间复杂度是 $O(n + m)$。
最后模拟栈排序的过程需要 $O(n)$ 的计算量。
因此总时间复杂度是 $O(n^2)$。
C++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
bool g[N][N];
int color[N];
bool dfs(int u, int c)
{
color[u] = c;
for (int j = 1; j <= n; j ++ )
{
if (!g[u][j]) continue;
if (color[j])
{
if (color[j] == c) return false;
}
else if (!dfs(j, 3 - c)) return false;
}
return true;
}
bool check(char a, char b)
{
if (a == b) return true;
if (a > b) swap(a, b);
return a == 'b' && b == 'c' || a == 'a' && b == 'd';
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
f[n + 1] = n + 1;
for (int i = n; i; i -- ) f[i] = min(a[i], f[i + 1]);
for (int i = 1; i <= n; i ++ )
for (int j = i + 1; j <= n; j ++ )
if (a[i] < a[j] && f[j + 1] < a[i])
g[i][j] = g[j][i] = true;
for (int i = 1; i <= n; i ++ )
if (!color[i] && !dfs(i, 1))
{
puts("0");
return 0;
}
string res;
stack<int> stk1, stk2;
for (int i = 1, cur = 1; i <= n; i ++ )
{
if (color[i] == 1) stk1.push(a[i]), res += 'a';
else stk2.push(a[i]), res += 'c';
while (stk1.size() && stk1.top() == cur || stk2.size() && stk2.top() == cur)
{
if (stk1.size() && stk1.top() == cur) stk1.pop(), cur ++, res += 'b';
else stk2.pop(), cur ++, res += 'd';
}
}
for (int i = 0; i < res.size();)
{
int j = i + 1;
while (j < res.size() && check(res[i], res[j])) j ++ ;
sort(res.begin() + i, res.begin() + j);
i = j;
}
for (auto c: res)
printf("%c ", c);
return 0;
}
本题代码已更正,现在可以正确输出最小字典序的方案了。
这个图的遍历是 $n^2$ 的,可以改一下
最后还有sort,时间复杂度O(nlogn)也算上吧
反悔贪心能做到O(N)吗,通过交换栈来实现,强调同一区间不会被交换两次,如果交换两次,之前那次解决的问题会重新出现。在我的代码里反映为交换后出现d2>d1;
hack数据:
5
2 3 1 4 5
输出:a c a b b a d b a b
大佬很强。要入第一个栈时,要先检查是否会使这个栈不合法,不合法的时候需要先 pop
代码已更正。
似乎有数据是错的?某个case是
10
10 2 8 1 7 9 3 4 5 6,
case提供的答案是
a a c a b b c a a b a b a b a b d d b b,
但是正确答案应该是
a a c a b b c a a b a b a b a b b b d d .
(最后4个数字弹出的顺序不对)
答案是a a c a b b c a a b a b a b a b d d b b,
最后1栈剩下的是10 9两个数,2栈剩下的是8 7 两个数,肯定要先弹出2栈的再出1栈的,所以最后4个顺序是 ddbb吧
代码已更正。
我觉得这篇题解的正确性证明得还不错呀qwq
小声bb:不像某谷有几个结论很难的贪心题,题解把证明过程草草带过,整篇题解就是模拟一下样例,连理解都存在问题 (逃
我觉得这个题目也是结论更重要,而某谷竟然把它当做二分图染色的例题?
当然只是个别情况,不能以一概全,大部分时候洛谷还是很友好的qwq
这题好像数据和题面都改了,居然不是多测了..
是的,替换成了NOIP原题描述和官方数据。
y神,提供一份hack数据:1 5 2 4 1 3 5
正确输出:a c a b b a b a d b
您的输出:a c a b b a b d a b
在洛谷P1155题解中看到的
题目要求的输入是一个1-n的排列啊。请问这个输入是不是不合法?
1
5
2 4 1 3 5
抱歉,描述不清晰。。。
这道题好难啊,不懂当年考场上的人是怎么过的
代码已更正。
大佬,如果n太大了,应该不能用个g[N][N]来存边吧
对滴