MX 暑假集训 7.22
はるのはるか(Faraway Spring) feat.Dazbee / sasakure.UK
思维题,感觉是我不会做的类型。
Square Root of Permutation
题意
定义一个置换(这里就是排列)\(q\) 的平方为一个置换 \(p\),满足 \(p_i=q_{q_i}\),写作 \(p=q^2\)。
给定一个长度为 \(n\) 的置换 \(p\),你需要找到一个置换 \(q\) 满足 \(q^2=p\),无解输出 \(-1\)。
\(1\le n\le 10^6\)。
solution
神秘置换题,使我的大脑旋转。
经典思维,\(i\rightarrow q_i\) 连边,那么形成了若干的环,我们考虑一次对 \(q\) 的平方操作使这些环怎么变化。
首先观察到平方操作其实就是对每个位置同时将其出边指向的元素换到当前位置。
考虑一个长度为 \(k\) 的环,组成为 \(a_1\rightarrow a_2\rightarrow \dots \rightarrow a_k\rightarrow a_1\)。
对 \(k\) 的奇偶性分类讨论。
- 若 \(k\) 为奇数,那么平方后仍形成一个环,为 \(a_1\rightarrow a_3\rightarrow a_5\rightarrow \dots \rightarrow a_k\rightarrow a_2 \rightarrow a_4\rightarrow \dots \rightarrow a_{k-1} \rightarrow a_1\);
- 若 \(k\) 为偶数,那么平方后形成两个大小相等的环,分别为 \(a_1\rightarrow a_3\rightarrow a_5\rightarrow \dots \rightarrow a_{k-1}\rightarrow a_1\) 和 \(a_2 \rightarrow a_4\rightarrow \dots \rightarrow a_{k} \rightarrow a_2\)。
那么到这里就好做了,考虑 \(p\) 的所有环,若是奇环则由一个 \(q\) 的奇环得来(当然也可能由一个偶环分裂成两个奇环,但是直接构造奇环显然不劣),若是偶环则寻找大小相同的偶环配对,组成 \(q\) 中的更大的偶环,找不到则无解。
时间复杂度 \(O(n)\)。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n;
int a[N],vis[N],ans[N];
vector<vector<int>> tmp[N];
vector<int> e[N];
inline void add(int x,int y) {ans[x]=y;}
inline void work(vector<int> &vec) {
int sz=vec.size()-1,mx=sz/2;
vector<int> x,y;
f(i,0,sz) {
if (i<=mx) x.push_back(vec[i]);
else y.push_back(vec[i]);
}
f(i,0,mx-1) add(x[i],y[i]),add(y[i],x[i+1]);
add(x[mx],x[0]);
}
inline void merge(vector<int> &x,vector<int> &y) {
int sz=x.size()-1;
f(i,0,sz) {
add(x[i],y[i]);
if (i!=sz) add(y[i],x[i+1]);
else add(y[i],x[0]);
}
}
int main() {
read(n);
f(i,1,n) read(a[i]);
f(i,1,n) {
if (vis[i]) continue;
int now=i;
vector<int> vec;
while (!vis[now]) {
vis[now]=1;
vec.push_back(now);
now=a[now];
}
tmp[vec.size()].push_back(vec);
}
f(i,1,n) {
if (i&1) {for (vector<int> x:tmp[i]) work(x);}
else {
if (tmp[i].size()&1) return puts("-1"),0;
while (!tmp[i].empty()) {
vector<int> x=tmp[i].back();
tmp[i].pop_back();
vector<int> y=tmp[i].back();
tmp[i].pop_back();
merge(x,y);
}
}
}
f(i,1,n) printf("%d ",ans[i]);
return 0;
}
[NOI Online 2022 提高组] 讨论
题意
给定 \(n\) 个集合,令集合大小之和为 \(m\),你需要找出两个集合 \(S_x,S_y\) 满足 \(S_x\cap S_y \ne \emptyset \land S_x\not\subseteq S_y\land S_y\not\subseteq S_x\),或输出不存在。
\(1\le n\le 10^6,1\le m\le 2\times 10^6\)。
solution
我们考虑钦定一个顺序,按集合大小从小到大排序,将相等的集合去重后,发现前面的集合一定不可能覆盖后面的集合,从小到大考虑一个集合,我们取出它的所有元素,并标记这些元素,若一个元素上有其他集合的标记,那么考虑这个标记的集合是否被当前集合覆盖,若不被覆盖则找到答案,若被覆盖则舍弃这个标记的集合,因为后面的集合如果能覆盖当前集合,那么一定也能覆盖标记的集合,而后面的集合不可能被当前集合覆盖,所以删掉标记的集合不会影响答案。
时间复杂度 \(O(m)\)。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n;
int id[N],sz[N],vis[N];
vector<int> tmp[N],t[N];
inline void solve() {
read(n);
f(i,1,n) {
read(sz[i]);
tmp[i].clear();
f(t,1,sz[i]) {
int x;read(x);
tmp[i].push_back(x);
}
t[sz[i]].push_back(i);
vis[i]=0;
}
int now=1;
f(i,0,n) {
for (int x:t[i]) id[now++]=x;
t[i].clear();
}
f(t,1,n) {
int i=id[t];
vector<int> did;
for (int x:tmp[i]) {
if (vis[x]) did.push_back(vis[x]),sz[vis[x]]--;
vis[x]=i;
}
for (int j:did) if (sz[j]) {
puts("YES");
printf("%d %d\n",i,j);
return;
}
}
puts("NO");
}
int main() {
int T;
read(T);
while (T--) solve();
return 0;
}
种树
题意
有 \(n\) 个坑排成一排,在第 \(i\) 个坑种树可以获得 \(a_i\) 的贡献,相邻两个坑不能同时种树,求至多种 \(k\) 棵树的最大贡献。
\(1\le k\le n\le 3\times 10^5\)。
solution
可以考虑一个费用流建模,源点建约 \(\frac{n}{2}\) 个辅助点(首尾的点比较特殊),第 \(i\) 个辅助点向 \(i\times 2-1,i\times 2\) 连边,即向 \((i,i+1)\) 属于一个辅助点,其中 \(i\) 为奇数,汇点同理,只不过 \(i\) 为偶数,以下是 \(n=6\) 时的建模方法。
S
1 2 3
1 2 3 4 5 6
| | | | | |
1 2 3 4 5 6
1 2 3 4
T
第二行为 \(S\) 的辅助点,第 \(6\) 行为 \(T\) 的辅助点,第 \(3,5\) 行其中每一条边表示每个坑种树的获利,每条边流量为 \(1\),这样就变成了一个费用流模型。
那么有很多优良的性质就体现出来了,首先具有凸性,可以直接做 wqs 二分,其次由于费用流本质在找增广路,所以可以证明 \(k\) 增大时选的集合不会发生很大的变化,即只是进行了简单的“替换”,所以用个反悔贪心也可以维护。
老师还讲了一种分治 dp 的方法,利用凸性进行两边 dp 的合并,没听懂喵。
还拓展了相邻 \(d\) 个位置不能种树的做法,似乎是做了另一个费用流模型,也证明出来有凸性。
Code
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define qwq Ff472130
#define ll long long
#define rg register
#define f(i,l,r) for (rg int i=l;i<=r;i++)
const int N=3e5+10;
const int inf=1e18;
int n,k;
int l[N],r[N],vis[N];
ll ans,v[N];
struct node{
ll val;int id;
inline bool operator <(const node &x)const {return val<x.val;}
}now;
priority_queue<node> q;
int main(){
scanf("%d%d",&n,&k);
f(i,1,n) {
scanf("%lld",&v[i]);
l[i]=i-1;r[i]=i+1;
q.push((node){v[i],i});
}
r[0]=1;l[n+1]=n;
f(i,1,k) {
while (vis[q.top().id]) q.pop();
now=q.top();
q.pop();
if (now.val<0) break;
ans+=now.val;
vis[l[now.id]]=vis[r[now.id]]=1;
v[now.id]=v[l[now.id]]+v[r[now.id]]-v[now.id];
l[now.id]=l[l[now.id]];
r[now.id]=r[r[now.id]];
l[r[now.id]]=now.id;
r[l[now.id]]=now.id;
q.push((node){v[now.id],now.id});
}
printf("%lld",ans);
return 0;
}
Bombs
题意
给定一个长度为 \(n\) 的排列 \(p\),对于一个炸弹位置集合 \(S\),考虑以下过程:
首先有一个空集合 \(A\),对于 \(i\) 从 \(1\) 到 \(n\):
- 将 \(p_i\) 加入 \(A\);
- 若 \(i\) 上有炸弹,则删除 \(A\) 中最大的元素。
定义 \(S\) 的权值为最后得到的 \(A\) 中的最大值。
初始时 \(S\) 为空,给定排列 \(q\),\(i\) 从 \(1\) 到 \(n-1\),输出初始空集的权值以及依次将 \(q_i\) 加入 \(S\) 后 \(S\) 的权值。
\(2\le n\le 3\times 10^5\)。
solution
老师还是讲了一个费用流模型,将 \(1\) 到 \(n\) 连成一条链,大编号向小编号连边,容量极大费用为 \(0\),\(i\) 向 \(T\) 连容 \(1\) 费 \(p_i\) 的边,每次加入一个位置在 \(i\) 的炸弹,相当于 \(S\) 向 \(i\) 连容 \(1\) 费 \(0\) 的边,最后求最大费用最大流,匹配上的边即为炸弹炸掉的边,然后用一个线段树维护了这个过程。
还有一种做法是双指针,显然答案单调不增,那么维护当前答案为 \(x\),\(x\) 合法则说明可以找到一个位置 \(i\),满足 \(i\) 后面不小于 \(x\) 的数的个数比炸弹数多,证明显然,那么双指针维护,线段树做前缀加或减即可。
时间复杂度 \(O(n\log n)\)。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3e5+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n;
int a[N],mp[N];
struct Segment_Tree {
int mx[N<<2],tag[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void modify(int x,int k) {mx[x]+=k;tag[x]+=k;}
inline void push_down(int x) {
if (!tag[x]) return;
int k=tag[x];tag[x]=0;
modify(ls(x),k);
modify(rs(x),k);
}
inline void update(int x,int y,int l,int r,int now,int k) {
if (x<=l&&y>=r) return modify(now,k);
push_down(now);
int mid=l+r>>1;
if (x<=mid) update(x,y,l,mid,ls(now),k);
if (y>mid) update(x,y,mid+1,r,rs(now),k);
mx[now]=max(mx[ls(now)],mx[rs(now)]);
}
}tr;
int main() {
read(n);
f(i,1,n) read(a[i]),mp[a[i]]=i;
int now=n;
f(i,1,n) {
while (tr.mx[1]<=0) tr.update(1,mp[now--],1,n,1,1);
printf("%d ",now+1);
int x;read(x);
tr.update(1,x,1,n,1,-1);
}
return 0;
}
Family Photos
题意
有 \(n\) 堆卡牌,每堆卡牌有两张卡牌,分为堆顶和堆底,小 A 和小 B 进行取卡牌游戏,对于每个堆,只有取走堆顶的卡牌后才能取堆底的卡牌,每张卡牌有两个值 \(a,b\),小 A 先手,轮流取卡牌,轮到一个人时可以选择跳过,当所有卡牌都被取走或跳过后游戏结束,令小 A 的收益为他拿走所有卡牌 \(a\) 值之和,小 B 的收益为他拿走所有卡牌 \(b\) 值之和,令小 A 的收益为 \(x\),小 B 的收益为 \(y\),那么小 A 希望最大化 \(x-y\) 的值,小 B 希望最小化这个值,求双方都采取最优策略时 \(x-y\) 的值。
\(1\le n\le 10^5\)。
solution
挺好玩的题,切了。
对于一堆牌,令堆顶的牌值为 \(a_1,b_1\),堆底为 \(a_2,b_2\)。
考虑每张牌,若有 \(a_1-b_2<a_2-b_1\),说明对于小 A,他可以暂时放着这张牌不管,因为如果小 B 拿走堆顶,小 A 能够得到更多的贡献,同时我们将这个不等式移项,得到 \(a_1+b_1<a_2+b_2\),说明小 B 也会暂时放着这张牌不管。
那么我们考虑 \(a_1+b_1\ge a_2+b_2\),可以看作将所有 \(b\) 值都先计入答案,一张牌对于小 A 的贡献为 \(a+b\),对小 B 没有贡献,于是小 A 肯定会按 \(a+b\) 降序拿走每张牌,而小 B 为了最小化小 A 拿走的贡献,轮到他时他也会拿走 \(a+b\) 值最大的牌,那么如何处理堆底的牌对堆顶的牌的依赖关系呢,实际上如果当前想要取走一张堆底的牌,那么由于 \(a_1+b_1\ge a_2+b_2\),那么堆顶的牌一定会先被取走,所以不需要考虑这个依赖关系,直接排序即可。
回到 \(a_1+b_1<a_2+b_2\) 的部分,虽然双方拿走堆顶都比对方拿走堆顶的贡献劣,但是既然对方肯定不会拿走堆顶,那么考虑一方拿走堆顶对他的贡献是否为正,即对于小 A,若有 \(a_1-b_2>0\),则小 A 拿走堆顶可以获得正收益,与其放着两个人都不愿碰,不如直接拿走堆顶,这样会比都不碰这堆牌更优,对小 B 也同理,一张牌不可能同时对小 A 与小 B 都造成正收益,因为如果都造成正收益,则说明 \(a_1>b_2,b_1>a_2\),那么就推出 \(a_1+b_1>a_2+b_2\),与前置条件相悖。
有一些牌取走堆顶对双方都不会有正收益,此时双方都会跳过,所以跳过这堆牌即可。
时间复杂度 \(O(n\log n)\)。
Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n,cnt;
struct Node{int a,b,v;}p[N];
inline bool cmp(Node x,Node y) {return x.v>y.v;}
int main() {
read(n);
ll ans=0;
f(i,1,n) {
int a1,b1,a2,b2;
read(a1);read(b1);read(a2);read(b2);
if (a1+b1>a2+b2) {
p[++cnt]={a1,b1,a1+b1};
p[++cnt]={a2,b2,a2+b2};
}
else {
if (a1-b2>0) ans+=a1-b2;
if (b1-a2>0) ans-=b1-a2;
}
}
sort(p+1,p+1+cnt,cmp);
f(i,1,cnt) {
if (i&1) ans+=p[i].a;
else ans-=p[i].b;
}
printf("%lld\n",ans);
return 0;
}
[清华集训 2017] 小 Y 和二叉树
题意
给定一棵二叉树结构,你需要为其确定一个根和每个节点的左右儿子,使得得到的二叉树中序遍历字典序最小,输出最小字典序。
\(1\le n\le 10^6\)。
solution
由于求的是最小字典序,那么肯定先需要第一位最小,考虑哪些节点可能成为中序遍历第一个遍历到的点。
显然要求这个点度数不超过 \(2\),且它没有左子节点。
找到最小的节点,令其为 \(x\),先钦定 \(x\) 为根,令 \(a_i\) 为 \(i\) 子树内度数不超过 \(2\) 的节点编号最小值。
若 \(x\) 度数为 \(1\),令另一个节点为 \(y\),有两种方案,分别为将 \(y\) 放到右子树和将 \(y\) 视为父亲,若视为右子树,则下一个被遍历到的点最小为 \(a_y\),若视为父亲则下一个遍历 \(y\),若 \(a_y<y\) 则视为右子树更优,否则视为父亲,说明一下 \(a_y=y\) 的情况,此时 \(y\) 度数不超过 \(2\),若度数为 \(1\) 则两个方案完全相同,若度数为 \(2\),令 \(z\) 为 \(y\) 连向的另一个节点,若将 \(y\) 看作 \(x\) 的右子树,则 \(z\) 一定作为 \(y\) 的右子树,而将 \(y\) 视为 \(x\) 的父亲,则 \(z\) 可能是 \(y\) 的右子树也可能是 \(y\) 的父亲,显然将 \(y\) 视为 \(x\) 父亲一定不劣。
若 \(x\) 度数为 \(2\),令连向的两个点分别为 \(y,z\),则比较 \(a_y\) 与 \(a_z\) 的大小,小的作为 \(x\) 右子树,另一个节点的情况其实与 \(x\) 完全相同,都是不能有左子树的情况下构造最小字典序,所以递归解决即可。
发现上面全部操作所需的 \(a\) 数组实际上与初始选定 \(x\) 为根处理出来的 \(a\) 数组完全一样,所以每次判断右子树和父亲时是 \(O(1)\) 的,总时间复杂度 \(O(n)\)。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;
inline void read(int &x) {
x=0;
char ch=getchar();
while (ch<48) ch=getchar();
while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}
int n;
int a[N],d[N];
vector<int> e[N];
bitset<N> vis;
inline void dfs(int now,int fa) {
a[now]=(d[now]<3)?(now):inf;
for (int v:e[now]) if (v^fa) {
dfs(v,now);
a[now]=min(a[now],a[v]);
}
}
inline void work(int now) {
vis.set(now);
vector<int> nxt;
for (int v:e[now]) if (!vis[v]) nxt.push_back(v);
if (nxt.empty()) return printf("%d ",now),void();
if (nxt.size()==1) {
int v=nxt.front();
if (a[v]<now) work(v);
printf("%d ",now);
if (a[v]>now) work(v);
return;
}
int x=nxt.front(),y=nxt.back();
if (a[x]>a[y]) swap(x,y);
work(x);
printf("%d ",now);
work(y);
}
inline void solve(int now) {
vector<int> nxt;
vis.set(now);
printf("%d ",now);
for (int v:e[now]) if (!vis[v]) nxt.push_back(v);
if (nxt.empty()) return;
if (nxt.size()==1) {
int y=nxt.front();
if (a[y]<y) work(y);
else solve(y);
return;
}
int x=nxt.front(),y=nxt.back();
if (a[x]>a[y]) swap(x,y);
work(x);solve(y);
}
int main() {
read(n);
f(i,1,n) {
read(d[i]);
f(t,1,d[i]) {
int x;read(x);
e[i].push_back(x);
}
}
int s=0;
f(i,1,n) if (d[i]<=2) {s=i;break;}
dfs(s,0);solve(s);
return 0;
}

浙公网安备 33010602011771号