MX 暑假集训 7.16

[Arcaea_BOFU2015] クロートーと星の観測者 - しーけー【FULL】

咋就最后一天了,要一个人留守青岛了。

最神秘的一场模拟赛,三个暴力获得了 \(100+90+80\) 的高分,预期应该是 \(90+20+60\)

T1: 网络改造

题意

给定一张有向图,对于每一条边,你可以将其反转或删除,第 \(i\) 条边反转和删除的代价分别为 \(x_i\)\(y_i\),对于每个点,你可以同时将与它相关的边(起点或终点是这个点)全部删除,对于第 \(i\) 个点代价是 \(c_i\),求将这张图变成 DAG 的最小代价。

\(1\le n\le 22,1\le m\le \frac{n(n-1)}{2}\)

solution

签到题,但是你怎么知道我 \(n^2 2^n\) 过了 \(n=22\)

考场上的理解有点复杂,直接对着官方题解讲算了。

一张 DAG 一定有一个拓扑序,一个点一定不能向拓扑序比它小的点连边,那么考虑对拓扑序进行 dp,令 \(dp_s\) 表示点集 \(s\) 为拓扑序的前缀时所需要的最小代价。

考虑如何加入一个点 \(i\) 到这个点集内,要求即为 \(i\) 不能向 \(s\) 内的任意点连边,直接枚举由 \(i\) 指向 \(s\) 内的点的边,将每条边取删除和反转中的较小值求和,再对直接删除 \(i\) 这个点的代价取较小值加入贡献即可。

为什么这样可行?如果一条 \(i\) 指向 \(j\) 的边,但在处理点 \(j\) 时删除了与 \(j\) 相连的所有边,那这条边不就不存在了吗?

确实会出现这个情况,但是容易发现,这种情况只会使答案不小于最优答案,而如果后加入 \(j\) 则答案是正确的,即我们钦定要删除的点在最后加入,得到的贡献就不会变多,最终取最小值时一定会将这种方案计入。

直接枚举边可以做到 \(O(n^2 2^n)\),直接冲过了 pretest,赛后发现没加优化也能冲过所有数据。

实际上我们在求 \(i\) 指向点集 \(s\) 的边权和(边权定义为删除和反转中的较小值),那么直接令 \(sum_{i,s}\) 表示边权和,每次取出 lowbit 递推即可优化至 \(O(n2^n)\)

代码略有偏差,我是倒着实现的,但整体思路一致。

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define qwq Ff472130
#define ll long long
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,r,l) for (int i=r;i>=l;i--)
constexpr int N=22+10;
constexpr int V=(1<<22)+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,m;
int c[N],dp[V];
int sum[N][V],E[N][N],num[V];

inline void solve() {
	read(n);read(m);
	f(i,1,m) {
		int u,v,x,y;
		read(u);read(v);read(x);read(y);
		E[v][u]=min(x,y);
	}
	f(i,1,n) read(c[i]),num[1<<(i-1)]=i;
	const int S=(1<<n)-1;
	f(i,1,n) f(j,1,S) {
		int lb=(j&-j),k=num[lb];
		sum[i][j]=sum[i][j^lb]+E[i][k];
	}
	f(s,1,S) {
		dp[s]=inf;
		int vs=s;
		while (vs) {
			int lb=(vs&-vs);
			vs^=lb;
			int j=num[lb],nxt=(s^(1<<(j-1)));
			dp[s]=min(dp[s],dp[nxt]+min(c[j],sum[j][nxt]));
		}
	}
	printf("%d\n",dp[S]);
	f(i,1,n) {
		f(j,1,n) E[i][j]=0;
		f(j,0,S) sum[i][j]=0;
	}
}

int main() {
	freopen("transform.in","r",stdin);
	freopen("transform.out","w",stdout);
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

T2: 七彩

题意

给定 \(n\) 个节点的有向图,这个有向图由 \(n\) 个颜色不同的环构成,你需要找出一个简单环,满足环上每条边颜色互不相同。

\(1\le n\le 3000,2n\le m\le \frac{n(n-1)}{2}\)

solution

何意味,不加任何剪枝的暴搜直接 \(90\) 分,加了一点优化就能过。

我们考虑每种颜色保留一条边,这样只需要找到环即可。

我们发现,源点对每个颜色连边,每个颜色对在它的环上的点连边,每个点向汇点连边,这样跑最大匹配,保留每种颜色在其匹配点上的出边,分类讨论。

若存在完美匹配,则每个点有一条出边,必然能找出一棵基环树,再找出这个环是容易的。

若不存在完美匹配,我们找一个未匹配环上的点,从它出发,一定不可能走到没有出边的点,因为若走到了没有出边的点(即未匹配点),则可以通过将这些经过的点进行调整,使得初始未匹配的环得到一个匹配点,这样就不满足最大匹配的前提了,而每个点都有出边则一定能找到一个环。

dinic 跑最大匹配,时间复杂度 \(O(n\sqrt m)\)\(O(n^2)\)

题解还介绍了一种更为神秘的做法。

直接搜索,和暴力区别在于我们进入一个点搜完后,若没有找到合法的环,则标记其为已访问过的点,将不会再被 dfs,无论这个点前面是否有一条路径,无论进入这个点时前面的颜色有没有使用过,都不会再次遍历到这个点。

正确性题解说通过归纳是对的,但是我并不知道为什么是对的,感觉这个不重复搜索特别神秘。

时间复杂度 \(O(n+m)\),即 \(O(n^2)\)

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define qwq Ff472130
#define ll long long
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,r,l) for (int i=r;i>=l;i--)
constexpr int N=3e3+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,ret;
int p[N],did[N];
struct Edge{int v,c,id1,id2;};
vector<Edge> e[N],ans;
bitset<N> vis;

int st=0;
inline bool dfs(int now) {
	did[now]=1;
	for (Edge E:e[now]) {
		int v=E.v,c=E.c;
		if (vis[c]) continue;
		if (did[v]==1) {
			ret=v;
			ans.push_back(E);
			return 1;
		}
		if (did[v]) continue;
		vis.set(c);
		if (dfs(v)) {
			ans.push_back(E);
			if (ret==now) {
				printf("%d\n",(int)(ans.size()));
				reverse(ans.begin(),ans.end());
				for (Edge E:ans) printf("%d %d\n",E.id1,E.id2);
				exit(0);
			}
			return 1;
		}
		vis.reset(c);
	}
	did[now]=2;
	return 0;
}

int main() {
    freopen("color.in","r",stdin);
    freopen("color.out","w",stdout);
	read(n);
	f(i,1,n) {
		int m;read(m);
		f(j,1,m) read(p[j]);
		f(j,1,m-1) e[p[j]].push_back({p[j+1],i,i,j});
		e[p[m]].push_back({p[1],i,i,m});
	}
	f(i,1,n) if (!did[i]) dfs(i);
	puts("0");
	return 0;
}

T3: Durability of Giant Robot

题意

你在玩一款战斗游戏,你需要歼灭 \(n\) 个敌人。

\(n\) 个敌人从左到右站成一排,从左到右第 \(i\) 个敌人高度为 \(h_i\)

假设你的英雄站在最左边,高度为 \(H\),防御力为 \(D\)

任意时刻,对于从左到右第 \(i\) 名敌人,你能攻击他当且仅当:

  • 对于任意 \(1\le j<i\),且第 \(j\) 名敌人存活,有 \(H>h_j\)

对于第 \(i\) 名敌人,他能攻击到你当且仅当:

  • 对于任意 \(1\le j<i\),且第 \(j\) 名敌人存活,有 \(h_i>h_j\)

每个时刻你的攻击和敌人的攻击同时发动,若同时攻击你的敌人数量超过 \(D\),则游戏结束,你失败了。

\(m\) 次询问,每次给定 \(H\),求 \(D\) 至少为多少,你才能击败所有敌人。

\(1\le n,m\le 2.5\times 10^5\)

solution

期望得分 \(60\) 的做法获得了 \(80\) 分的高分,讲讲暴力是怎么打的吧。

首先想的是贪心,但是 pretest 错了几个,感觉不是很可行,于是想到二分。

一个敌人能攻击到角色当且仅当他是前缀最大值。

对于一个 \(H\),我们考虑如何判定一个 \(D\) 是否合法。

先将每个 \(h_i\ge H\) 的位置拿出来,考虑以 \(i\) 开始的后缀,统计其前缀最大值的个数,对于一段 \(h_i<H\) 的“小怪”,我们可能可以留下一个最高的“小怪”,让他成为“挡箭牌”,使得后面的一些小怪没办法攻击到我们,那么能将一段“小怪”中保留一个到下一段的前提条件是,这一段后面的一段“大怪”,即 \(h_i>=H\) 的敌人,以它开头的后缀,前缀最大值的个数小于 \(D\),这样可以在保留一只而使前缀最大值个数不超过 \(D\),从左向右扫的同时对“小怪”段统计前缀最大值的个数,判定是否都不超过 \(D\),于是我们得到了一个 \(O(n)\) 的 checker。

复杂度是 \(O(nm\log n)\) 的,可以获得 \(40\) pts。

能不能优化呢?由于是 \(m\) 次询问,于是我们离线下来做整体二分。

发现按照 \(H\) 从小到大排序后,答案的 \(D\) 一定是单调递减的,令按 \(H\) 排序后 \([L,R]\) 内的询问对应的答案为 \([l,r]\),令 \(mid=\frac{l+r}{2}\) 那么我们考虑哪些询问答案 \(\le mid\),哪些 \(>mid\)

考虑再做一次二分,对 \([L,R]\) 二分,找到第一个满足 \(mid\) 的防御力足够通过的位置 \(M\),那么 \([L,M-1]\) 内询问对应答案在 \([mid+1,r]\) 内,\([M,R]\) 内询问答案在 \([l,mid]\) 内。

这样看似优化了时间复杂度,实际上最坏时间复杂度还是 \(O(nm\log n)\) 的,但是随机数据下前缀最大值期望个数是 \(O(\log n)\) 个的,所以整体二分应该可以做到 \(O(n\log n\logm)\) 的优秀复杂度,拼上性质,我们获得了 \(60\) pts 的高分。

实际上这份代码因为没被卡多冲过了 \(5\) 个点,得到了 \(80\) pts,对于答案很接近的一些询问是可以快速解决的。

Code
#include<cstdio>
#include<algorithm>
#include<bitset>
#include<vector>
using namespace std;
#define qwq Ff472130
#define ll long long
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,r,l) for (int i=r;i>=l;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,m;
int a[N];

namespace Ff1 {
    int vis[N],ed[N],nxt[N];
    inline void init(int h) {
        f(i,1,n) vis[i]=ed[i]=nxt[i]=0;
        int mx=inf,cnt=0;
        static int st[N];
        int top=0;
        F(i,n,1) if (a[i]>=h) {
            vis[i]=top;
            while (top&&st[top]<=a[i]) top--;
            st[++top]=a[i];
            ed[i]=top;
            if (a[i+1]<h) nxt[i]=i+1;
            else nxt[i]=nxt[i+1],vis[i]=max(vis[i],vis[i+1]);
        }
    }

    inline bool check(int h,int D) {
        int mx=0,cnt=0;
        f(i,1,n) {
            int x=a[i];
            if (x<h) {if (x>mx) cnt++,mx=x;}
            else {
                if (cnt+ed[i]>D) return 0;
                if (vis[i]<D&&a[nxt[i]]<mx) cnt=1;
                else mx=0,cnt=0;
            }
            if (cnt>D) return 0;
        }
        return 1;
    }

    struct ques{int h,id;}p[N];
    inline bool cmp(ques x,ques y) {return x.h<y.h;}
    int mp[N<<1],ans[N],Len;

    inline int find_mid(int l,int r,int v) {
        int res=r+1;
        while (l<=r) {
            int mid=l+r>>1;init(p[mid].h);
            if (check(p[mid].h,v)) res=mid,r=mid-1;
            else l=mid+1;
        }
        return res-1;
    }

    inline int calc(int h,int l,int r) {
        int res=0;init(h);
        while (l<=r) {
            int mid=l+r>>1;
            if (check(h,mid)) res=mid,r=mid-1;
            else l=mid+1;
        }
        return res;
    }

    inline void solve(int l,int r,int L,int R) {
        if (L>R) return;
        if (l==r) {
            f(i,L,R) ans[p[i].id]=l;
            return;
        }
        if (R-L+1<=2) {
            f(i,L,R) ans[p[i].id]=calc(p[i].h,l,r);
            return;
        }
        int mid=l+r>>1,M=find_mid(L,R,mid);
        solve(mid+1,r,L,M);
        solve(l,mid,M+1,R);
    }

    inline void solve() {
        f(i,1,n) mp[++Len]=a[i];
        f(i,1,m) read(p[i].h),p[i].id=i,mp[++Len]=p[i].h;
        sort(mp+1,mp+1+Len);
        Len=unique(mp+1,mp+1+Len)-mp-1;
        f(i,1,n) a[i]=lower_bound(mp+1,mp+1+Len,a[i])-mp;
        f(i,1,m) p[i].h=lower_bound(mp+1,mp+1+Len,p[i].h)-mp;
        sort(p+1,p+1+m,cmp);
        solve(1,n,1,m);
        f(i,1,m) printf("%d\n",ans[i]);
    }
}

namespace Ff2 {
    int ans[N],ed[N];
    struct ques{int h,id;}p[N];
    inline bool cmp(ques x,ques y) {return x.h<y.h;}

    inline void solve() {
        static int st[N];
        int top=0,mx=0,now=0;
        F(i,n,1) {
            while (top&&st[top]<=a[i]) top--;
            st[++top]=a[i];
            ed[i]=top;
            if (ed[i]>mx) mx=ed[i],now=i;
        }
        f(i,1,m) read(p[i].h),p[i].id=i;
        sort(p+1,p+1+m,cmp);
        f(i,1,m) {
            while (now&&p[i].h>a[now]) now--;
            ans[p[i].id]=max({ed[1],ed[now],ed[now+1]});
        }
        f(i,1,m) printf("%d\n",ans[i]);
    }
}

inline bool checkB() {
    int k1=n,k2=n;
    f(i,2,n) if (a[i]>=a[i-1]) {k1=i-1;break;}
    F(i,n-1,1) if (a[i]<=a[i-1]) {k2=i;break;}
    return k1==k2;
}

int main() {
//    freopen("ex_contra3.in","r",stdin);
//    freopen("out.out","w",stdout);

    freopen("contra.in","r",stdin);
    freopen("contra.out","w",stdout);

    read(n);read(m);
    f(i,1,n) read(a[i]);
    if (checkB()) Ff2::solve();
    else Ff1::solve();
    return 0;
}

/*
chmod +x test.sh
./test.sh -s contra.cpp -t 2 -m 512
*/
posted @ 2026-07-16 17:04  Ff472130  阅读(1)  评论(0)    收藏  举报