Codeforces Round #709 补题

Codeforces Round #709

这场乱搞场,很迷。

D.Playlist

$n$个数字按顺序给定$a_1,a_2\dots a_n$,从$1$开始循环,设当前保留数字$x$,下一个相邻的未删除数字位$y$,若$gcd(a_x,a_y)=1$,则删除$y$,并从下一个数字开始操作。请按顺序输出被删掉的数字的编号。

思路

这题很迷,一开始感觉是按$gcd$分块+链表贪心。

看了别人的题解既不理解正确性又不明白复杂度分析。

我们可以发现,若一段区间$[l,r]$内连续的$gcd>1$成立,那么该区间内只有右端点$r$才会移除别人,而$[l,r)$只可能会被别人移除。

那么我们使用队列来维护$gcd=1$的点对(放入点对中第一个点),本质还是模拟。

原本相邻的点对$(x,y)$,若$gcd(a_x,a_y)=1$,那么移除$y$,并且让$x$指向剩下点中$y$的下个点(维护相邻点对 ),将$x$放入队尾。

否则若$gcd(a_x,a_y)>1$,那么点$x$永远不会移除别人,我们可以不考虑$x$点产生的效果,直接让$x$出队,这个也是终止条件。

复杂度分析

没错困扰了我一下午。

一轮跑下来只有相邻$gcd=1$的点存在于队列中,花费$T(n)$。

每一轮,$gcd>1$连续块内只会有一个元素保留下来,而若$gcd(a_x,a_y)=1$,则会删掉$y$($y$原本处于队列中,是目前$x$的相邻点),每完全进行一轮操作,每次队列内元素数量至少减半。

相当于对数列
$$
n+\dfrac{n}{2}+\dfrac{n}{4}+\dfrac{n}{8}+\dots \dfrac{n}{2^∞}
$$
求和,$n$趋向于无穷时$=2n$。

感觉是$O(n)$的。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//#pragma comment(linker, "/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
//#define int ll
typedef pair<int,int>pii;
#define ff first
#define ss second
#define debug(x) std:: cerr << #x << " = " << x << std::endl;
const int maxn=2e5+10,inf=0x3f3f3f3f,mod=1000000007;
const ll INF=0x3f3f3f3f3f3f3f3f;
const double eps=1e-9;
//#define DEBUG//#define lowbit(x) ((x) & -(x))//<<setiosflags(ios::fixed)<<setprecision(9)
void read(){}
template<typename T,typename... T2>inline void read(T &x,T2 &... oth) {
x=0; int ch=getchar(),f=0;
while(ch<'0'||ch>'9'){if (ch=='-') f=1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
if(f)x=-x;
read(oth...);
}
int a[maxn],nxt[maxn];
signed main(signed argc, char const *argv[])
{
int t,n;
cin>>t;
while(t--)
{
cin>>n;
vector<bool> vis(n+1,false);
list<int> lst;
for(int i=1;i<=n;i++)
{
cin>>a[i];
nxt[i]=i+1;
lst.push_back(i);
}
nxt[n]=1;
vector<int>ans;
int cnt=0;
while(!lst.empty())
{
int x=lst.front();
lst.pop_front();
if(vis[x])
continue;
if(__gcd(a[x],a[nxt[x]])==1)
{//移除nxt[x]
ans.push_back(nxt[x]);
vis[nxt[x]]=1;//已删除
nxt[x]=nxt[nxt[x]];//指向保留的下个点
lst.push_back(x);//放到末尾
}
}
cerr<<"wtf"<<endl;
cout<<ans.size()<<' ';
for(int &i:ans)
cout<<i<<' ';
cout<<endl;
}
return 0;
}
/*
9999
8
2 2 2 2 3 3 3 3

8
1 2 3 4 5 6 7 8

8
2 3 5 7 11 13 17 19

ans = 2,4,6,8,3,7,5
*/
/*
*到最后肯定保留元素相邻gcd>1
*如果某个点x与x+1之间的gcd>1
*使用队列维护当前相邻两个gcd=1的数对
*每次取出来一个点x,若被删除则跳过
*当跑过一轮后,只有gcd(a[x],a[nxt_x])=1的x才被保留下来
*每个点最多只会被删一次
*/

E.Skyline Photo

给定两个$n$长数组$h_1,h_2\dots h_n$与$b_1,b_2,\dots b_n$,$h$保证为$n$的全排列,你要将$[1,n]$分为不为空的任意大小块,每一个块的贡献为该块内$h$最小的位置对应的$b$,求出最大贡献和。

思路

先写出$O(n^2)$的转移方程,$f(x,y)$为区间$[x,y]$的贡献。

1
2
3
for i←1 to n
for j1 to i-1
dp[i]=max(dp[i],dp[j]+f(j+1,i))

考虑优化这个$dp$,我们可以发现对于每个点$i$,他所控制的范围向前截止到离他最近的且$h$值比它更小的位置,即$[p+1,i]$,这一步可以用单调递增的单调栈维护每个值前面最近小于他的值。

那么如果我们对于$i$选择其对应的断点$j$落在$[p+1,i]$这一段区间里,$b[i]$的权值是一定计入$dp[i]$的,而我们只是要让这个值最大化,很明显$dp[i]=\operatorname{max}\limits_{j=p+1}^{i-1}(dp[j])+b[i]$,此时转变成一个查询区间最值的问题。

而若$i$所在的分割块的左端点在$p$的左面,即$j\in [1,p]$,那么$b[i]$一定不会计入贡献,因为该块内此时有$b$值比它更小的权值了,令$dp[i]=dp[p]$即可转移(如果令$dp[i]=max(dp[1:p])$的话可能不是合法解)。

若$i$左面没有比$h[i]$更矮的,直接讨论一下即可。

思路流程:推出来一个复杂度较差的$dp$->尝试观察性质,发现每个点的控制范围并且想到单调栈->优化转移过程。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//#pragma comment(linker, "/STACK:102400000,102400000")
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll
typedef pair<int,int>pii;
#define ff first
#define ss second
#define debug(x) std:: cerr << #x << " = " << x << std::endl;
const int maxn=3e5+10,inf=0x3f3f3f3f,mod=1000000007;
const ll INF=0x3f3f3f3f3f3f3f3f;
struct node
{
int val;
} tree[maxn<<2];
int n;
inline void pushup(int rt)
{
tree[rt].val=max(tree[rt<<1].val,tree[rt<<1|1].val);
}
inline void pushdown(int rt)
{
return;
}
void build(int rt,int l,int r)
{
if(l==r)
{
tree[rt].val=-INF;
return;
}
int mid=(l+r)>>1;
build(rt<<1,l,mid);
build(rt<<1|1,mid+1,r);
pushup(rt);
}
void modify(int rt,int x,int val,int l=1,int r=n)
{
if(l==r)
{
tree[rt].val=val;
return;
}
pushdown(rt);
int mid=(l+r)>>1;
if(x<=mid)
modify(rt<<1,x,val,l,mid);
else
modify(rt<<1|1,x,val,mid+1,r);
pushup(rt);
}
int query(int rt,int ql,int qr,int l=1,int r=n)
{
if(r<ql||l>qr)
return -INF;
if(ql<=l&&r<=qr)
return tree[rt].val;
pushdown(rt);
int mid=(l+r)>>1;
return max(query(rt<<1,ql,qr,l,mid),query(rt<<1|1,ql,qr,mid+1,r));
}
int h[maxn],b[maxn],dp[maxn];
int stk[maxn],top=0;
signed main(signed argc, char const *argv[])
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>h[i],dp[i]=-INF;
for(int i=1;i<=n;i++)
cin>>b[i];
build(1,1,n);
for(int i=1;i<=n;i++)
{
while(top&&h[stk[top]]>h[i])
top--;
dp[i]=dp[i-1]+b[i];
if(top)
{
int p=stk[top];
dp[i]=max(dp[i],query(1,p+1,i-1)+b[i]);//找到这个断点
dp[i]=max(dp[i],dp[p]);//被前面比它小的覆盖掉
}
else{//本身是最矮的
dp[i]=max(dp[i],b[i]);
dp[i]=max(dp[i],query(1,1,i-1)+b[i]);
}
stk[++top]=i;
modify(1,i,dp[i]);
}
cout<<dp[n]<<endl;
return 0;
}
/*
5
5 4 3 1 2
-10 -10 -5 -1 -5

*/
/*
*找最美丽的一组照片
*n个建筑,高度h,美丽度b,分成n组照片
*一张照片的美丽度=h最小的那一个的b
*如果每个b都是正数,那么每个建筑单独一张照片
*dp[i]代表[1,i]区间内可获得的最大美丽度
*dp[i]=max
*区间快速求最小值及下标可以用st表
*单调栈维护每个点左边小于这个值的最近点
*线段树维护区间最值,单点修改
*/