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
*/

浙公网安备 33010602011771号