2026.9 做题记录3
9.14
进行了一个 dp 套 dp 的学习。
Hero meet devil
solution
考虑 LIS 暴力 dp 的转移:令 \(f_{i,j}\) 表示第一个串考虑到第 \(i\) 个位置,第二个串考虑到第 \(j\) 个位置的 LIS 长度,那么有转移 \(f_{i,j}\leftarrow \max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+[s_i=t_j])\),考虑维护这个 dp,令 \(g_{i,j}\) 表示构造了长度为 \(i\) 的答案串,其中 \(f_i\) 的状态为 \(j\) 的方案数。
状态 \(j\) 为一个数组 \(f_i\),虽然这个数组值域很大,但是注意到相邻两项最多差 \(1\),所以直接对 \(f_i\) 差分数组进行状压即可得到状态 \(j\)。
转移时枚举下一个位置的字符,考虑 \(f_i\) 增加这个字符后得到新的 \(f_{i+1}\) 数组状态是什么,这个可以预处理出来,即可得到答案。
时间复杂度 \(O(2^nm\left\lvert \Sigma \right\rvert)\)。
Code
#include<cstdio>
#include<algorithm>
#include<cstring>
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=1000+10;
constexpr int V=(1<<16)+10;
constexpr int inf=1e9+10;
constexpr int mod=1e9+7;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}
int n,m;
char s[N];
int a[N],ans[N];
int nxt[V][5],f[2][V];
inline int calc(int s,int x) {
static int g[20],h[20];g[0]=h[0]=0;
f(i,1,n) g[i]=g[i-1]+((s>>(i-1))&1);
f(i,1,n) {
if (a[i]==x) h[i]=g[i-1]+1;
else h[i]=max(h[i-1],g[i]);
}
int ret=0;
f(i,1,n) if (h[i]>h[i-1]) ret|=(1<<(i-1));
return ret;
}
inline void solve() {
scanf("%s",s+1);
n=strlen(s+1);
scanf("%d",&m);
f(i,1,n) {
if (s[i]=='A') a[i]=1;
if (s[i]=='G') a[i]=2;
if (s[i]=='T') a[i]=3;
if (s[i]=='C') a[i]=4;
}
int S=(1<<n)-1;
f(i,0,S) f(j,1,4) nxt[i][j]=calc(i,j);
f[0][0]=1;
f(i,0,m-1) {
const int d0=(i&1),d1=(d0^1);
f(s,0,S) if (f[d0][s]) {
f(k,1,4) add(f[d1][nxt[s][k]],f[d0][s]);
f[d0][s]=0;
}
}
f(s,0,S) add(ans[__builtin_popcount(s)],f[m&1][s]);
f(i,0,n) printf("%d\n",ans[i]);
f(i,0,1) f(s,0,S) f[i][s]=0;
f(i,0,n) ans[i]=0;
}
int main() {
int T;scanf("%d",&T);
while (T--) solve();
return 0;
}
【MX-X28-T4】「FAOI-R12」落花雨
solution
首先最终得到字符串的每个字符都会对应原串的一段区间。
注意到长度不小于 \(4\) 的串都能转化为一个 \(0\) 字符或一个 \(1\) 字符。
考虑如何判定一个串是否可以由原串变成,考虑贪心地去判定,也就是尽量靠前地进行匹配,但是这样不一定优秀,不过由上面的性质发现如果可以将原串 \([1,i]\) 变成这个串的前缀,那么最优变化方式一定会选一个长度在 \(i,i+1,i+2\) 中的前缀进行匹配。
于是考虑维护 \(f_{i,s}\) 表示答案串匹配了原串 \([1,i]\),\(i+1,i+2\) 是否可以作为合法匹配位置的方案数,转移时枚举下一个需要的字符并找到第一个可以变成这个字符的位置即可。
时间复杂度线性。
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=2e5+10;
constexpr int inf=1e9+10;
constexpr int mod=998244353;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}
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;
char s[N];
inline bool nor(bool x,bool y) {return !(x||y);}
inline int calc(int l,int r) {
if (r-l+1>=4) return 3;
if (l==r) return (1<<(s[l]-'0'));
if (l+1==r) return (1<<nor(s[l]-'0',s[r]-'0'));
int c0=s[l]-'0',c1=s[l+1]-'0',c2=s[r]-'0';
return (1<<(nor(nor(c0,c1),c2)))|(1<<nor(c0,nor(c1,c2)));
}
int dp[N][4];
inline void solve() {
read(n);
scanf("%s",s+1);
dp[0][0]=1;
ll ans=0;
f(i,0,n) f(s,0,3) if (dp[i][s]) {
vector<int> vec;
vec.push_back(i);
f(j,0,1) if ((s>>j)&1) vec.push_back(i+j+1);
int flag_p=0;
for (int pos:vec) flag_p|=(pos>n);
if (flag_p) continue;
f(k,0,1) {
int nxt=0;
for (int p:vec) F(x,6,1) if (p+x<=n&&(calc(p+1,p+x)>>k)&1) nxt|=(1<<(p+x-i));
if (!nxt) continue;
int mn=__builtin_ctz(nxt);
int t=0;
if ((nxt>>(mn+1))&1) t|=1;
if ((nxt>>(mn+2))&1) t|=2;
add(dp[i+mn][t],dp[i][s]);
}
if (i&&(n-i>=3||i==n||((s>>(n-i-1))&1))) ans+=dp[i][s];
}
printf("%d\n",(int)(ans%mod));
f(i,0,n) f(s,0,3) dp[i][s]=0;
}
int main() {
int T;
read(T);
while (T--) solve();
return 0;
}
9.15
上升
solution
神秘 dp,根本不会。
令 \(l_i\) 为 \(i\) 以前最后一个 \(p\) 不为 \(0\) 位置 \(p\) 的值。
令 \(f_{i,j,k}\) 表示考虑前 \(i\) 个数,填入的第 \(i\) 个数 \(q_i \le l_i\),且 \(i\) 以前还有 \(j\) 个 \(\le l_i\) 的数没有填过,并且这些未填过的数中有 \(k\) 个小于填入的 \(q_i\) 的权值和。
令 \(g_{i,j,k}\) 表示考虑前 \(i\) 个数,填入的第 \(i\) 个数 \(q_i > l_i\),且 \(i\) 以前还有 \(j\) 个 \(\le l_i\) 的数没有填过,并且前面已经填入了 \(k\) 个 \(>l_i\) 且 \(<q_i\) 的数。
若 \(p_i\) 为 \(0\),有:
注意 \(f\) 的转移需要消耗一个空位,对应 \(j+1\),用 \(k\) 的限制来判断是否上升。
同样用 \(k\) 的限制来判断是否上升。
\(p_i\) 不为 \(0\) 时,值域上 \([l_{i-1},p_i]\) 闭合,此时需要枚举有几个元素落在了这段值域上,从而进行后面的转移,令 \(d\) 个之前填过 \(>l_{i-1}\) 的数落在这段值域上,\(len=p_i-l_{i-1}-1\)。
同样利用 \(k\) 的限制判定是否上升。
用前缀和优化转移即可做到 \(O(n^3)\)。
实际上这个题我觉得这个状态设计得很巧妙,这类 dp 的难点也在于设计出类似如此的状态,虽然我做不到每道题都能够设计出这样的状态,但我还是试着解析一下此题状态的核心要点吧。
首先我们希望将这个排列确定下来,但是记录哪些位置确定了肯定是不可接受的,当我们用 \(L=l_i\) 作为分界线将 \(\le L\) 与 \(>L\) 的数分开后,可以得到两个不同的参考系。
一个是 \(\le L\) 中确定的数,这些数是唯一确定的,因为后面有值的 \(p\) 取值不可能是这些位置,所以我们将其固定下来,刻画为具体的数字。
另一个是 \(>L\) 中抽象的大小关系,它并不能确定下具体的值,但是可以刻画出上升的关系,这虽然不像上一个参考系那样稳定,但是在下一个有值的 \(p\) 限制下,值域闭合,将一段抽象的关系转化为了具体的数值,这正是巧妙之处,由于 \(>L\) 值域不可确定,就暂时刻画为一段抽象关系,在可确定时再具体刻画出来,抽象与具体的转化正是核心所在。
Code
#include "ascend.h"
#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--)
int mod;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}
int ascend(int c, int n, int m, std::vector<int> p,std::vector<int> w) {
mod=m;
p.insert(p.begin(),0);p.push_back(++n);
w.insert(w.begin(),1);w.insert(w.begin(),0);w.push_back(1);
vector<vector<int>> ept(n+2,vector<int>(n+2,0));
vector<vector<int>> f=ept,g=ept,sf=ept,sg=ept,C=ept;
f(i,0,n) {
C[i][0]=1;
f(j,1,i) add(C[i][j]=C[i-1][j],C[i-1][j-1]);
}
int L=0;
f[0][0]=1;
f(i,1,n) {
ll val=w[i];
f(j,0,L) {
sf[j][0]=f[j][0];sg[j][0]=g[j][0];
f(k,1,n) add(sf[j][k]=sf[j][k-1],f[j][k]),add(sg[j][k]=sg[j][k-1],g[j][k]);
}
f=ept;g=ept;
if (p[i]) {
int Len=p[i]-L-1;
f(j,0,L) F(k,Len,0) {
if (j+Len-k>n) break;
add(f[j+Len-k][j+Len-k],(sf[j][n]*val+sg[j][n]+(k?(sg[j][k-1]*(mod+val-1)):0))%mod*C[Len][k]%mod);
}L=p[i];
}
else {
f(j,0,L) {
f(k,0,j) f[j][k]=(sf[j+1][k]*val+sf[j+1][n]+mod-sf[j+1][k]+sg[j+1][n])%mod;
int M=min(i-1-L+j,n);
f(k,0,M) g[j][k]=(sf[j][n]*val+sg[j][n]+(k?(sg[j][k-1]*(mod+val-1)):0))%mod;
}
}
}
return f[0][0];
}
9.16
进行了一个 JOIST 的 vp。
Bitaro's Travel 2
solution
一开始想了个分治套 Kruskal 上维护线段树优化最短路的,后面发现好像没有分治的必要。
直接构建 Kruskal 重构树,一个点通过一次操作能够去到的点就是权值不超过其高度加 \(L\) 的最浅祖先内子树叶子节点,于是直接倍增维护 \(2^k\) 次操作后最浅能到达哪个祖先,并找到最小次数使得起点跳到一个子树内包括终点的祖先节点上即可。
时间复杂度 \(O((n+q)\log n)\),\(n=H\times W\)。
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 M=2e6+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,q,L,idx;
int mp[N];
struct ques{int sx,sy,ex,ey,id;};
vector<vector<int>> id;
namespace Kruskal {
int Etot,cnt;
struct Edge{int u,v,w;}e[M];
inline bool cmp(Edge x,Edge y) {return x.w<y.w;}
struct dsu {
int f[N];
inline int find(int x) {
if (f[x]==x) return x;
return f[x]=find(f[x]);
}
}ds;
int val[N],dep[N],dfn[N],siz[N],dfn_idx;
int f[N][22],g[N][22];
vector<int> T[N];
inline int dfs(int now,int fa) {
dep[now]=dep[fa]+(siz[now]=1);
dfn[now]=++dfn_idx;
if (T[now].empty()) return g[now][0];
int mx=0;
for (int v:T[now]) {
int k=dfs(v,now);
siz[now]+=siz[v];
if (!mx||dep[k]<dep[mx]) mx=k;
}
return g[now][0]=mx;
}
inline void solve() {
static int dx[]={0,0,1,-1};
static int dy[]={1,-1,0,0};
f(i,1,n) f(j,1,m) f(k,0,3) {
int x=i+dx[k],y=j+dy[k];
if (x<1||x>n||y<1||y>m||id[i][j]>id[x][y]) continue;
e[++Etot]={id[i][j],id[x][y],max(mp[id[i][j]],mp[id[x][y]])};
}
sort(e+1,e+1+Etot,cmp);
f(i,1,idx*2) ds.f[i]=i;cnt=idx;
f(i,1,Etot) {
int u=ds.find(e[i].u),v=ds.find(e[i].v),w=e[i].w;
if (u==v) continue;
cnt++;val[cnt]=w;
f[u][0]=f[v][0]=cnt;
T[cnt].push_back(u);
T[cnt].push_back(v);
ds.f[u]=ds.f[v]=cnt;
}
f(j,1,20) f(i,1,cnt) f[i][j]=f[f[i][j-1]][j-1];
f(i,1,idx) {
int now=i,D=mp[i]+L;
F(j,20,0) if (f[now][j]&&val[f[now][j]]<=D) now=f[now][j];
g[i][0]=now;
}
dfs(cnt,0);
f(j,1,20) f(i,1,cnt) g[i][j]=g[g[i][j-1]][j-1];
}
inline int query(int x,int y) {
int ans=0;y=dfn[y];
F(j,20,0) {
int fa=g[x][j];
int L=dfn[fa],R=dfn[fa]+siz[fa]-1;
if (L<=y&&y<=R) continue;
ans+=(1<<j);x=g[x][j];
}
ans++;x=g[x][0];
int L=dfn[x],R=dfn[x]+siz[x]-1;
if (L<=y&&y<=R) return ans;
return -1;
}
}
int main() {
read(n);read(m);read(L);
id=vector<vector<int>>(n+2,vector<int>(m+2,0));
f(i,1,n) f(j,1,m) {
id[i][j]=++idx;
read(mp[idx]);
}
Kruskal::solve();
read(q);
while (q--) {
int sx,sy,ex,ey;
read(sx);read(sy);read(ex);read(ey);
printf("%d\n",Kruskal::query(id[sx][sy],id[ex][ey]));
}
return 0;
}
9.17
补了一下昨天前两题的题解。
[JOIST 2025] 展览会 3 / Exhibition 3
题意
给定一个长度为 \(n\) 的序列 \(a\),给定 \(m\) 个区间 \([l_i,r_i]\),你需要重排 \(a\),令 \(b_i=\max\limits_{j=l_i}^{r_i} a_j\),你需要使 \((b_1,b_2,\dots,b_m)\) 字典序最大,输出最大字典序。
\(1\le n,m\le 10^5\)。
solution
深深感受到自己的弱小。
考虑从大到小枚举每种 \(a\) 的权值,并让当前这种权值尽可能覆盖前面的还未被覆盖过的区间。
于是有了最朴素的暴力:枚举每种权值,令这种权值在 \(a\) 中出现 \(cnt\) 次,按编号从小到大考虑每个未被覆盖的区间,若加入后,覆盖已选的全部区间(含新加入的)所需点数不超过 \(cnt\),就将其加入,判断覆盖一组区间所需的最少点数可以按照右端点升序排序,并依次在未被覆盖区间的最小右端点上放一个点进行覆盖,根据贪心可以知道这是对的。
考虑合法的区间是什么样的,不难发现合法的区间由一段前缀以及后面一些零散区间构成,看起来这并没有什么可用的信息,但实际上覆盖这段前缀所需的点数恰好为 \(cnt\),否则下一个未被覆盖的区间一定会被选入,这也就说明了后面的零散区间不会改变覆盖所需要的最小点数。
考虑如何找到这个前缀,目前我们有了 \(O(c\log c)\) 判定 \(c\) 个区间是否可行的方法,于是考虑倍增,令当前第一个未被覆盖区间编号为 \(x\),找到最大的 \(k\) 满足覆盖编号在 \([x,x+2^k-1]\) 的所有区间所需要的点数不超过 \(cnt\),且覆盖编号在 \([x,x+2^{k+1}-1]\) 的所有区间所需要的点数超过 \(cnt\),判定是否可行直接使用暴力的判定方法,然后在 \([x+2^k-1,x+2^{k+1}-1)\) 内二分出最长的合法前缀。
这个做法看起来很神秘,但是这样做能保证复杂度,设这段前缀长度为 \(c\),通过倍增,我们用 \(O(c\log c)\) 的时间确定了二分范围,二分进行了 \(O(\log c)\) 次,每次判定需要时间为 \(O(c\log c)\),所以用 \(O(c\log^2 c)\) 的时间找到了这段前缀,由于每个区间只被覆盖一次,所以相当于我们用了 \(O(c\log^2 c)\) 的时间覆盖了 \(O(c)\) 个区间,复杂度是正确的,个人认为这个倍增非常巧妙。
前缀确定后,还需要找出所有加入后所需点数不增加的零散区间,考虑如何找到这些区间。
一个经典的结论:按照上面的贪心,我们得到 \(cnt\) 个点最右边可以放置到的位置,反过来,按照左端点降序排序,找到最大的未被覆盖区间的左端点并在端点上放一个点覆盖此区间,得到 \(cnt\) 个点最左边可以放置到的位置,令第 \(i\) 个点最左边与最右边分别能放到 \(pL_i,pR_i\),那么这个点放在 \([pL_i,pR_i]\) 的任何一个位置,都能通过调整其它的点来做到覆盖所有区间。
并且 \([pL_i,pR_i]\) 是两两无交的,否则将点放在交集会使点数减少。
那么可以得到:\([L,R]\) 加入后不会增加点数,当且仅当 \([L,R]\) 与某个 \([pL,pR]\) 有交。
将这些区间分成以下几类:
- \([L,R]\) 完全包含其中一个 \([pL,pR]\);
- \([L,R]\) 不包含任何一个 \([pL,pR]\),且与两个 \([pL,pR]\) 有交;
- \([L,R]\) 不包含任何一个 \([pL,pR]\),且与一个 \([pL,pR]\) 有交。
加入一个区间后,可能对某些 \([pL,pR]\) 有影响,但是一定是使某些 \([pL,pR]\) 变小,因为限制更严了。
考虑加入上面每种区间的影响。
对于第一种区间,根据上面的贪心,它并不会影响 \(pL,pR\),因为按照右端点贪心时,会先在与它有交的 \(pR\) 处放一个点,从而将 \([L,R]\) 覆盖,不会参与后面的贪心,\(pL\) 同理,并且发现无论 \([pL,pR]\) 怎么缩小,\([L,R]\) 一定包含 \([pL,pR]\),所以这类区间无论如何都可以被加入,可以放到最后同时处理,由于都会加入,所以不用考虑编号,用线段树维护 \(L<pL\land pR<R\) 的二维偏序即可。
对于第二种区间,有 \(pL_{j}\le L\le pR_{j}<pL_{j+1}\le R\le pR_{j+1}\),那么根据上面的贪心,在 \(pR_j\) 处放置点时将 \([L,R]\) 覆盖,在 \(pL_{j+1}\) 处放置点时将 \([L,R]\) 覆盖,所以 \([pL,pR]\) 并不会变动。
对于第三种区间,若左端点落在 \([pL,pR]\) 内,则需要将 \(pL\leftarrow L\),若右端点落在 \([pL,pR]\) 内,则需要将 \(pR\leftarrow R\),同时当前 \(pL,pR\) 修改后,后面的 \(pR\) 或前面的 \(pL\) 可能也会被修改。
先考虑如何找到编号最小的第二种和第三种区间,发现这两种区间必然有端点落在某个 \([pL,pR]\) 上,于是用线段树在每个端点处维护编号即可,对所有 \([pL,pR]\) 找到端点在里面的最小编号,放进小根堆中每次取出堆顶更新,并在 \([pL,pR]\) 修改后重新获取最小的编号即可。
再来考虑 \(pR\) 的更新,\(pL\) 的情况是对称的。
若 \(pR_j\) 更新,则 \(pR_{j+1}\) 会变成所有 \(L>pR_j\) 的区间中 \(R\) 的最小值,令 \(c\) 为当前权值覆盖区间的总数量,由于每个区间对 \(pR_j\) 的更新最多贡献一次,所以 \(pR\) 的总修改量均摊只有 \(O(c)\),用线段树维护加入区间中每个 \(L\) 对应的最小 \(R\),在更新 \(pR_j\) 时检查 \(pR_{j+1}\) 是否需要更新,若需要则递归更新 \(pR_{j+1}\),否则停止更新,因为 \(j+1\) 后面点的 \(pR\) 不可能继续更新。
注意从堆中取出区间时要判断是否与 \([pL,pR]\) 还有交,因为 \([pL,pR]\) 可能已经修改过了。
最终时间复杂度 \(O(n\log^2 n)\),精细实现预处理出 \(2^k\) 长度的排序可以做到 \(O(n\log n)\)。
代码为 \(O(n\log^2 n)\),一些地方与题解略有差异,实现上有很多细节,码量也很大,最好整理清楚思路再写。
Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
#include<queue>
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=1e5+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],ans[N];
bitset<N> vis;
struct Node{int l,r,id;}p[N];
inline bool cmp1(Node x,Node y) {return x.r<y.r;}
inline bool cmp2(Node x,Node y) {return x.l>y.l;}
inline bool cmp3(Node x,Node y) {return x.id<y.id;}
inline bool check(vector<Node> &tmp,int mxt) {
sort(tmp.begin(),tmp.end(),cmp1);
int cnt=0,pR=0;
for (Node x:tmp) if (x.l>pR) cnt++,pR=x.r;
sort(tmp.begin(),tmp.end(),cmp3);
return cnt<=mxt;
}
struct Segment_Tree1 {
// find L<pl and pr<R [L,R]->L mx=max(R)
int mx[N<<2];
vector<Node> tr[N];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void build(int l,int r,int now) {
if (l==r) return mx[now]=(tr[l].empty()?0:tr[l].back().r),void();
int mid=l+r>>1;
build(l,mid,ls(now));
build(mid+1,r,rs(now));
mx[now]=max(mx[ls(now)],mx[rs(now)]);
}
inline void build() {
sort(p+1,p+1+m,cmp1);
f(i,1,m) tr[p[i].l].push_back(p[i]);
sort(p+1,p+1+m,cmp3);
build(1,n,1);
}
inline int query(int x,int y,int l,int r,int now,int k) {
if (mx[now]<=k) return 0;
if (l==r) {
while (!tr[l].empty()&&vis[tr[l].back().id]) tr[l].pop_back();
mx[now]=(tr[l].empty()?0:tr[l].back().r);
if (mx[now]<=k) return 0;
int ret=tr[l].back().id;
tr[l].pop_back();
mx[now]=(tr[l].empty()?0:tr[l].back().r);
return ret;
}
int mid=l+r>>1,ret=0;
if (x<=mid) ret=query(x,y,l,mid,ls(now),k);
if (!ret&&y>mid) ret=query(x,y,mid+1,r,rs(now),k);
mx[now]=max(mx[ls(now)],mx[rs(now)]);
return ret;
}
}tr1;
struct Segment_Tree2 {
// find pl<=L<=pr or pl<=R<=pr id min [L,R]->L,R
int mn[N<<2];
vector<int> tr[N];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void build(int l,int r,int now) {
if (l==r) return mn[now]=(tr[l].empty()?inf:tr[l].back()),void();
int mid=l+r>>1;
build(l,mid,ls(now));
build(mid+1,r,rs(now));
mn[now]=min(mn[ls(now)],mn[rs(now)]);
}
inline void build() {
f(i,1,(n<<2)) mn[i]=inf;
F(i,m,1) {
tr[p[i].l].push_back(p[i].id);
tr[p[i].r].push_back(p[i].id);
}
build(1,n,1);
}
inline void update(int x,int l,int r,int now) {
if (l==r) {
while (!tr[l].empty()&&vis[tr[l].back()]) tr[l].pop_back();
mn[now]=(tr[l].empty()?inf:tr[l].back());
return;
}
int mid=l+r>>1;
if (x<=mid) update(x,l,mid,ls(now));
else update(x,mid+1,r,rs(now));
mn[now]=min(mn[ls(now)],mn[rs(now)]);
}
inline int query(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return mn[now];
int mid=l+r>>1,res=inf;
if (x<=mid) res=query(x,y,l,mid,ls(now));
if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
return res;
}
}tr2;
struct Segment_Tree3 {
//update [pl,pr] [L,R] L=x R min
int mx[N<<2];
inline void build() {f(i,1,(n<<2))mx[i]=inf;}
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void update(int x,int l,int r,int now,int k) {
if (l==r) {
if (k!=inf) mx[now]=min(mx[now],k);
else mx[now]=inf;
return;
}
int mid=l+r>>1;
if (x<=mid) update(x,l,mid,ls(now),k);
else update(x,mid+1,r,rs(now),k);
mx[now]=min(mx[ls(now)],mx[rs(now)]);
}
inline int query(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return mx[now];
int mid=l+r>>1,res=inf;
if (x<=mid) res=query(x,y,l,mid,ls(now));
if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
return res;
}
}tr3;
struct Segment_Tree4 {
//update [pl,pr] [L,R] R=x L max
int mx[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void update(int x,int l,int r,int now,int k) {
if (l==r) {
if (k!=0) mx[now]=max(mx[now],k);
else mx[now]=0;
return;
}
int mid=l+r>>1;
if (x<=mid) update(x,l,mid,ls(now),k);
else update(x,mid+1,r,rs(now),k);
mx[now]=max(mx[ls(now)],mx[rs(now)]);
}
inline int query(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return mx[now];
int mid=l+r>>1,res=0;
if (x<=mid) res=query(x,y,l,mid,ls(now));
if (y>mid) res=max(res,query(x,y,mid+1,r,rs(now)));
return res;
}
}tr4;
struct Que_Node {
int id,pid;
inline bool operator <(const Que_Node &x)const {return id>x.id;}
};
inline void insert_id(vector<Node> &tmp,int id) {
vis.set(id);
tmp.push_back(p[id]);
tr2.update(p[id].l,1,n,1);
tr2.update(p[id].r,1,n,1);
tr3.update(p[id].l,1,n,1,p[id].r);
tr4.update(p[id].r,1,n,1,p[id].l);
}
inline bool check_cap(int id,int pid,int *pL,int *pR) {
if (pL[pid]>p[id].r||p[id].l>pR[pid]) return 0;
return 1;
}
inline void update_pL(int id,int *pL,int *pR,int k,priority_queue<Que_Node> &q) {
int new_id=tr2.query(pL[id],pR[id],1,n,1);
if (new_id!=inf) q.push({new_id,id});
if (id==1||pL[id]==1) return;
int mxL=tr4.query(1,pL[id]-1,1,n,1);
if (mxL>pL[id-1]) pL[id-1]=mxL,update_pL(id-1,pL,pR,k,q);
}
inline void update_pR(int id,int *pL,int *pR,int k,priority_queue<Que_Node> &q) {
int new_id=tr2.query(pL[id],pR[id],1,n,1);
if (new_id!=inf) q.push({new_id,id});
if (id==k||pR[id]==n) return;
int mnR=tr3.query(pR[id]+1,n,1,n,1);
if (pR[id+1]>mnR) pR[id+1]=mnR,update_pR(id+1,pL,pR,k,q);
}
inline void solve(vector<Node> &tmp,int k,int val) {
for (Node &k:tmp) {
int id=k.id;
vis.set(k.id);
tr2.update(p[id].l,1,n,1);
tr2.update(p[id].r,1,n,1);
tr3.update(p[id].l,1,n,1,p[id].r);
tr4.update(p[id].r,1,n,1,p[id].l);
}
static int pL[N],pR[N];
int now=0;
sort(tmp.begin(),tmp.end(),cmp1);
for (Node x:tmp) if (x.l>pR[now]) pR[++now]=x.r;
now=k;
sort(tmp.begin(),tmp.end(),cmp2);
for (Node x:tmp) if (now==k||x.r<pL[now+1]) pL[now--]=x.l;
priority_queue<Que_Node> q;
f(i,1,k) {
int id=tr2.query(pL[i],pR[i],1,n,1);
if (id!=inf) q.push({id,i});
}
while (!q.empty()) {
int id=q.top().id,pid=q.top().pid;
q.pop();
if (vis[id]||!check_cap(id,pid,pL,pR)) {
int new_id=tr2.query(pL[pid],pR[pid],1,n,1);
if (new_id!=inf) q.push({new_id,pid});
continue;
}
insert_id(tmp,id);
int new_id=tr2.query(pL[pid],pR[pid],1,n,1);
if (new_id!=inf) q.push({new_id,pid});
if (pL[pid]<=p[id].l&&p[id].r<=pR[pid]) {
if (pL[pid]!=p[id].l) pL[pid]=p[id].l,update_pL(pid,pL,pR,k,q);
if (pR[pid]!=p[id].r) pR[pid]=p[id].r,update_pR(pid,pL,pR,k,q);
continue;
}
if (pL[pid]<=p[id].l&&p[id].l<=pR[pid]) {
if (pid<k&&check_cap(id,pid+1,pL,pR)) continue;
if (pL[pid]!=p[id].l) pL[pid]=p[id].l,update_pL(pid,pL,pR,k,q);
}
if (pL[pid]<=p[id].r&&p[id].r<=pR[pid]) {
if (pid>1&&check_cap(id,pid-1,pL,pR)) continue;
if (pR[pid]!=p[id].r) pR[pid]=p[id].r,update_pR(pid,pL,pR,k,q);
}
}
f(i,1,k) {
int L=pL[i],R=pR[i];
if (L==1) continue;
int id=tr1.query(1,L-1,1,n,1,R);
while (id) {
insert_id(tmp,id);
id=tr1.query(1,L-1,1,n,1,R);
}
}
for (Node &k:tmp) {
tr3.update(k.l,1,n,1,inf);
tr4.update(k.r,1,n,1,0);
ans[k.id]=val;
}
}
int check_id=1;
inline void solve(int mxt,int val) {
int now=check_id,pt=now-1;
vector<Node> tmp;
for (int k=1,pos=0;(pos=now+k-1)<=m;k<<=1) {
f(i,pt+1,pos) if (!vis[i]) tmp.push_back(p[i]);
if (check(tmp,mxt)) pt=pos;
else {
f(i,pt+1,pos) if (!vis[i]) tmp.pop_back();
break;
}
}
int l=pt,r=min(m,pt+(pt-now+1));
while (l<=r) {
int mid=l+r>>1;
f(i,pt+1,mid) if (!vis[i]) tmp.push_back(p[i]);
if (check(tmp,mxt)) l=mid+1,pt=mid;
else {
f(i,pt+1,mid) if (!vis[i]) tmp.pop_back();
r=mid-1;
}
}
if (pt==m) {
for (Node k:tmp) vis.set(k.id),ans[k.id]=val;
while (vis[check_id]) check_id++;
return;
}
solve(tmp,mxt,val);
while (vis[check_id]) check_id++;
}
int main() {
read(n);read(m);
f(i,1,n) {int x;read(x);a[x]++;}
f(i,1,m) read(p[i].l),read(p[i].r),p[i].id=i;
tr1.build();tr2.build();tr3.build();
F(i,n,1) if (a[i]) solve(a[i],i);
f(i,1,m) printf("%d\n",ans[i]);
return 0;
}
[JOIST 2025] 占卜 3 / Fortune Telling 3
solution
极致的状态压缩,太牛了这个题。
L=18
此题不好使用二进制来维护信息,因为 \(0,1\) 的个数不确定,考虑换一种表示方法,一个基础的想法就是用 \(x\) 个 \(0\) 与 \(y\) 个 \(1\) 表示 \(\binom{x+y}{x}\) 级别的信息。
我们想要传递一个组合数的形式,那么可以用隔板法传递一个数组,具体地,我们可以传递一个长度为 \(x+1\) 的数组,表示第一个 \(0\) 前面,从左到右两个 \(0\) 之间,最后一个 \(0\) 后面有几个 \(1\)。
但是我们得到的数可能是 \(0\) 也可能是 \(1\) 怎么办呢,考虑在前 \(890\) 个位置拿出 \(4\) 个 \(0\) 和 \(4\) 个 \(1\),\(0\) 放在前 \(1\) 放在后,我们想要传递的数是前 \(890\) 个数中 \(1\) 的个数,那么考虑后 \(10\) 个位置,若值为 \(0\) 则放到后面的 \(1\) 之间,若值为 \(1\) 则放到前面的 \(0\) 之间,解码时找到前面四个 \(0\) 和后面四个 \(1\),前面以 \(0\) 为界得到 \(5\) 个位置 \(1\) 的数量,后面以 \(1\) 为界得到 \(5\) 个位置 \(0\) 的数量,将每个位置数量相加,就可以得到一个长度为 \(5\),和为 \(10\) 的数组,能够表示 \(\binom{14}{4}>1000\) 的信息,对于后面 \(10\) 个位置的 \(1\),都出现在前面 \(0\) 的旁边,直接计入贡献即可。
如果前 \(890\) 个位置找不到 \(4\) 个 \(0\),则将后 \(10\) 个位置所有 \(1\) 放入,解码时发现 \(0\) 的个数不到 \(4\) 个,于是通过给出 \(1\) 的数量可以计算出所有 \(1\) 的数量。
前面找不到 \(4\) 个 \(1\) 的情况同理,这样做可以做到 \(L=18\),获得 \(71\) 的分数。
我认为打到 \(71\) 的分数已经很困难了,下面的优化有点不是人类了。
Anna
#include "Anna.h"
#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--)
inline vector<int> dfs(int &num,int sum,int pos,vector<int> now) {
if (pos==5) {
num--;
now.push_back(sum);
if (!num) return now;
return vector<int>();
}
f(i,0,sum) {
now.push_back(i);
vector<int> ret=dfs(num,sum-i,pos+1,now);
if (!ret.empty()) return ret;
now.pop_back();
}
return vector<int>();
}
void Anna(int N) {
int c0=0,c1=0,Len=0;
int now=DrawCard(-1),num=0;
f(i,1,890) {
if (now) {
num++;
if (c1<4) now=DrawCard(Len++),c1++;
else now=DrawCard(-1);
}
else {
if (c0<4) now=DrawCard(0),Len++,c0++;
else now=DrawCard(-1);
}
}
if (c0<4) {
f(i,1,10) {
if (now) now=DrawCard(Len++);
else now=DrawCard(-1);
}
return;
}
if (c1<4) {
f(i,1,10) {
if (now) now=DrawCard(-1);
else now=DrawCard(Len++);
}
return;
}
num++;
vector<int> tmp=dfs(num,10,1,vector<int>());
int p1=0,p2=4,pos=0;
while (!tmp[pos]) pos++,p1++,p2++;
f(i,1,10) {
if (now) now=DrawCard(p1++),p2++;
else now=DrawCard(p2++);
tmp[pos]--;
while (pos<=4&&!tmp[pos]) pos++,p1++,p2++;
}
}
Bruno
#include "Bruno.h"
#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--)
inline int dfs(vector<int> &ans,int sum,int pos,vector<int> now,int &ret) {
if (pos==5) {
ret++;
now.push_back(sum);
f(i,0,4) if (ans[i]!=now[i]) return -1;
return ret;
}
f(i,0,sum) {
now.push_back(i);
int res=dfs(ans,sum-i,pos+1,now,ret);
if (res!=-1) return res;
now.pop_back();
}
return -1;
}
int Bruno(int N, int L, std::vector<int> C) {
int c0=0,c1=0;
for (int x:C) {
if (x) c1++;
else c0++;
}
if (c0<4) return 886-c0+c1;
if (c1<4) return 14-c0+c1;
vector<int> A,B;
c0=0;c1=0;int p0=0,p1=0;L--;
f(i,0,L) {
if (!C[i]) c0++;
if (c0==4) {p0=i;break;}
}
F(i,L,0) {
if (C[i]) c1++;
if (c1==4) {p1=i;break;}
}
c0=0;c1=0;
int num=0;
f(i,0,p1-1) {
if (!C[i]) A.push_back(num),c0++,num=0;
else num++;
if (c0==5) break;
}
if (c0==4) A.push_back(num),num=0;
p0++;
while (p0<p1&&C[p0]) p0++;
f(i,p0,L) {
if (C[i]) B.push_back(num),c1++,num=0;
else num++;
if (c1==5) break;
}
if (c1==4) B.push_back(num),num=0;
vector<int> tmp;
f(i,0,4) tmp.push_back(A[i]+B[i]);
int sum1=0;
for (int x:C) sum1+=x;
return sum1-5+dfs(tmp,10,1,vector<int>(),num);
}
L=16
在前面的表示中,用了固定的 \(10\) 个 \(1\) 和 \(4\) 个 \(0\) 来表示 \(\binom{14}{4}\) 的状态,我们发现可以不固定组合数中 \(1\) 的数量,从而表示更多的状态。
具体地,从前面 \(892\) 个位置拿出 \(4\) 个 \(0\) 和 \(4\) 个 \(1\),要表示的数仍然为前面 \(892\) 个位置中 \(1\) 的数量,令这个数量为 \(c\)。
按顺序考虑 \(892\) 后面的每一个位置,令后面还剩下 \(lst\) 个位置,那么使用后面能够表示的状态数即为 \(\binom{lst+4}{4}\),若 \(c\ge \binom{lst+4}{4}\),则 \(c\leftarrow c-\binom{lst+4}{4}\),再将这个位置扔掉,由于这个位置可能是 \(1\),所以为了计入扔掉位置的贡献,当扔掉位置上是 \(1\) 时将这个贡献计入进 \(c\) 中,即 \(c\leftarrow c+1\)。
解码时和上面类似,解出一个数组后,这个数组的和即为实际插入的牌数,由此可知弃掉了多少张,及每张弃掉时减去的 \(\binom{lst+4}{4}\) 是多少,从而还原贡献,再加上桌上插入的那些牌中 \(1\) 的个数和这个数组本身对应的值即为答案,被弃掉的牌上的 \(1\) 已计入 \(c\) 中,无需额外考虑。
corner case 的解决方案和 \(L=18\) 是一样的。
这种方法能够表示 \(\sum\limits_{i=1}^{8}\binom{4+i}{4}=1286\) 的状态,即使最后扔掉牌使 \(c\) 变化会带来一定损耗,但解决 \(N=900\) 还是绰绰有余的。
做到了 \(L=16\),可以获得 \(81\) 的分数。
L=14
还能优化的地方在于,可以不固定组合数中 \(0\) 的数量。
首先还是从牌中拿出四个 \(0\),这四个 \(0\) 必须放的原因是需要确定一个基本的相对位置,否则难以找到一个标准来得到这个数组。
考虑前 \(890+k\) 张牌,其中 \(k\) 为拿出 \(1\) 的数量,将第 \(D_1,D_2,D_3,D_4\) 个出现的 \(1\) 拿出,若没有则不拿出,其中 \(D_1,D_2,D_3,D_4\) 由拿出 \(1\) 后剩余部分能够表示的状态数决定。
那么根据拿出 \(1\) 的数量,我们可以根据 \(1\) 的数量分为 \([D_1,D_2-1],[D_2,D_3-1],[D_3,D_4-1],[D_4,\infty )\),四种类别,每种类别利用 \(L=16\) 的方法计算出对应贡献即可。
令拿出 \(1\) 的个数为 \(k\),那么能够表示的信息位有 \(10-k\) 个,那么能够表示的信息量就有 \(\sum\limits_{i=1}^{10-k}\binom{k+i}{k}\)。
\(1\le k\le 4\) 时,四种 \(k\) 合计能表示的状态数有 \(1008\) 种,但是考虑到后面丢弃 \(1\) 带来的损耗,实际上能表示的信息量会略小,根据信息量,保守对 \(D\) 进行取值即可。
解码时找到前 \(4\) 个 \(0\),再考虑后面有几个 \(1\),若第四个 \(0\) 后面的 \(1\) 不超过 \(4\) 个,则它们必然全部用于分隔,否则分隔 \(1\) 的数量为恰好 \(4\),所以 \(1\) 的数量对 \(4\) 取 \(\min\) 的值即为放入作为分隔 \(1\) 的数量,同时也是组合数中 \(0\) 的数量。
和 \(L=16\) 一样解出一个数组,加上数组对应的数值并还原出减去的贡献即可。
若前面全是 \(0\),则后面也全放 \(0\),这样即可解出 \(1\) 的数量,这也是 \(k\) 不取 \(0\) 的原因(会与这种情况冲突)。
若前面拿不出 \(4\) 个 \(0\),那么后面只放 \(1\),同样可以解出 \(1\) 的数量。
于是做到了 \(L=14\),可以通过此题。
Anna
#include "Anna.h"
#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--)
inline vector<int> dfs(int &num,int sum,int pos,vector<int> now) {
if (pos==1) {
num--;
now.push_back(sum);
if (!num) return now;
return vector<int>();
}
f(i,0,sum) {
now.push_back(i);
vector<int> ret=dfs(num,sum-i,pos-1,now);
if (!ret.empty()) return ret;
now.pop_back();
}
return vector<int>();
}
void Anna(int N) {
int _D[]={1,31,165,463};
int now=DrawCard(-1);
int c0=0,c1=0,num=0,Len=0;
f(i,1,890+num) {
if (now) {
c1++;
if (num<4&&c1==_D[num]) now=DrawCard(Len++),num++;
else now=DrawCard(-1);
}
else {
c0++;
if (c0<=4) now=DrawCard(0),Len++;
else now=DrawCard(-1);
}
}
if (c0<4) {
f(i,895,900) {
if (now) now=DrawCard(Len++);
else now=DrawCard(-1);
}
return;
}
if (!c1) {
f(i,891,900) {
if (!now) now=DrawCard(0);
else now=DrawCard(-1);
}
return;
}
static int C[15][15];
f(i,0,13) {C[i][0]=1;f(j,1,i) C[i][j]=C[i-1][j]+C[i-1][j-1];}
int flag=0,i=890+num+1;c1-=_D[num-1];
while (i<=900) {
int lst=900-i+1;
if (c1>=C[lst+num][num]) c1-=C[lst+num][num];
else break;
c1+=now;now=DrawCard(-1);i++;
}c1++;
vector<int> tmp=dfs(c1,900-i+1,num+1,vector<int>());
int p0=0,p1=4,pos=0;
while (pos<=num&&!tmp[pos]) pos++,p0++,p1++;
for(;i<=900;i++) {
if (now) now=DrawCard(p0++),p1++;
else now=DrawCard(p1++);
tmp[pos]--;
while (pos<=num&&!tmp[pos]) pos++,p0++,p1++;
}
}
Bruno
#include "Bruno.h"
#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--)
inline int dfs(vector<int> &ans,int sum,int pos,vector<int> now,int &ret) {
if (pos==1) {
ret++;
now.push_back(sum);
for (int i=0;i<ans.size();i++) if (ans[i]!=now[i]) return -1;
return ret;
}
f(i,0,sum) {
now.push_back(i);
int res=dfs(ans,sum-i,pos-1,now,ret);
if (res!=-1) return res;
now.pop_back();
}
return -1;
}
inline int get_val(int x,int y) {
static int C[15][15];
f(i,0,13) {C[i][0]=1;f(j,1,i) C[i][j]=C[i-1][j]+C[i-1][j-1];}
int ret=0;
f(i,x+1,10-y) ret+=C[i+y][y];
return ret;
}
int Bruno(int N, int L, std::vector<int> C) {
int _D[]={1,31,165,463};
int c0=0,c1=0;
for (int x:C) c0+=(!x);
if (c0<4) return L-c0*2+890;
if (c0==L) return 14-c0;
vector<int> A,B;
c0=0;c1=0;int p0=0,p1=0;L--;
f(i,0,L) {
if (!C[i]) c0++;
if (c0==4) {p0=i;break;}
}
F(i,L,p0+1) {
if (C[i]) c1++,p1=i;
if (c1==4) {p1=i;break;}
}
int len=c1;
c0=0;c1=0;
int num=0;
f(i,0,p1-1) {
if (!C[i]) A.push_back(num),c0++,num=0;
else num++;
if (c0==len+1) break;
}
if (c0==len) A.push_back(num),num=0;
p0++;
while (p0<p1&&C[p0]) p0++;
f(i,p0,L) {
if (C[i]) B.push_back(num),c1++,num=0;
else num++;
if (c1==len+1) break;
}
if (c1==len) B.push_back(num),num=0;
int sum1=0,sum=0;
vector<int> tmp;
f(i,0,len) tmp.push_back(A[i]+B[i]),sum+=tmp[i];
for (int x:C) sum1+=x;
return sum1-len+dfs(tmp,sum,len+1,vector<int>(),num)-1+_D[len-1]+get_val(sum,len);
}
9.20
\(0+100+80=180\)。
[JOIST 2025] 集邮比赛 4 / Collecting Stamps 4
solution
签到题,切了。
先考虑固定一个起点。
考虑每种颜色,由于这种颜色第二次出现时前面的颜色种类数一定不少于第一次出现,所以这种颜色作为盖章卡右侧颜色最多能盖的盖章卡个数为其第二次出现位置前的颜色数,最终能盖的盖章卡个数即为每种颜色最多能盖的个数之和。
对于一次交换操作,交换了 \(i\) 和 \(i+1\) 位置,当 \(i\) 上的颜色为第二次出现且 \(i+1\) 位置颜色为第一次出现时,能使能盖的盖章卡数量增加 \(1\),不难发现当当前盖章卡个数不为 \(n^2\) 时必然存在一种交换方式能使盖章卡个数增加 \(1\)。
所以对于一个起点 \(i\),计算出初始能盖的盖章卡个数 \(w_i\),那么对于一个询问 \(k\),该起点的最小成本即为 \(c_i+\max(0,X\times (k-w_i))\)。
考虑快速求出每个起点对应的 \(w_i\),只需要考虑将第一个位置移到最后一个位置后盖章卡个数变化量为多少,破环为链,令第一个位置颜色出现位置为 \(L\),第二次出现位置为 \(R\),那么在 \([L,R)\) 内的所有颜色贡献会减少 \(1\),即减去 \([L,R)\) 内的颜色数,再加上第一个位置颜色的变化量,即减去 \([L,R]\) 内颜色数并加上 \(n\),由于每种颜色的出现次数为 \(2\),所以用数据结构维护每种颜色第二次出现位置即可。
对于每个询问,答案为 \(\min\limits_{i=1}^{n\times 2}(c_i+\max(0,X\times (k-w_i)))\),对 \(w\) 排序并离线询问同时按照 \(k\) 排序,对于 \(k\le w_i\) 和 \(k>w_i\) 单独维护贡献,双指针即可。
时间复杂度 \(O(n\log n+q\log q)\)。
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=2e6+10;
constexpr ll inf=(ll)(3e18)+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();
}
inline void readl(ll &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,X;
int a[N];
ll c[N],w[N];
struct Segment_Tree {
int s[N<<2];
inline int ls(int x) {return x<<1;}
inline int rs(int x) {return x<<1|1;}
inline void update(int x,int l,int r,int now,int k) {
if (l==r) return s[now]=k,void();
int mid=l+r>>1;
if (x<=mid) update(x,l,mid,ls(now),k);
else update(x,mid+1,r,rs(now),k);
s[now]=s[ls(now)]+s[rs(now)];
}
inline int query(int x,int y,int l,int r,int now) {
if (x<=l&&y>=r) return s[now];
int mid=l+r>>1,res=0;
if (x<=mid) res=query(x,y,l,mid,ls(now));
if (y>mid) res+=query(x,y,mid+1,r,rs(now));
return res;
}
}tr;
inline void solve_w() {
static int lst[N],L[N],R[N];
ll sum=0,num=0;
f(i,1,n) {
int x=a[i];
if (lst[x]) {
tr.update(i,1,n*2,1,1);
R[x]=i;sum+=num;
}
else L[x]=i,num++,lst[x]=1;
}w[1]=sum;
f(i,1,n-1) {
int x=a[i];
sum-=tr.query(L[x],R[x]-1,1,n*2,1);
sum-=R[x]-1-tr.query(1,R[x]-1,1,n*2,1)-(i-1);
tr.update(R[x],1,n*2,1,0);
tr.update(i+n,1,n*2,1,1);
L[x]=R[x];R[x]=i+n;sum+=n/2;
w[i+1]=sum;
}
}
ll ans[N],ed[N];
struct Node {ll c,w;}p[N],q[N];
inline bool cmp(Node x,Node y) {return x.w>y.w;}
int main() {
read(n);read(X);n*=2;
f(i,1,n) read(a[i]),a[i+n]=a[i];
f(i,1,n) readl(c[i]),c[i+n]=c[i];
solve_w();
f(i,1,n) p[i]={c[i],w[i]};ed[n+1]=inf;
sort(p+1,p+1+n,cmp);
F(i,n,1) ed[i]=min(ed[i+1],p[i].c-X*p[i].w);
read(m);
f(i,1,m) readl(q[i].w),q[i].c=i;
sort(q+1,q+1+m,cmp);
int now=1;ll mn=inf;
f(i,1,m) {
while (now<=n&&q[i].w<=p[now].w) {
mn=min(mn,p[now].c);
now++;
}
ans[q[i].c]=min(mn,ed[now]+q[i].w*X);
}
f(i,1,m) printf("%lld\n",ans[i]);
return 0;
}
[JOIST 2025] 宇宙怪盗 / Space Thief
题意
给定一张 \(n\) 个点 \(m\) 条边的连通无向图,有一个隐藏的起点 \(s\) 和一个隐藏的终点 \(t\),每次询问你可以给边定向,并询问定向后 \(s\) 能否到达 \(t\),你需要在 \(70\) 次询问内确定 \(s\) 和 \(t\) 分别是哪个点,保证 \(s\ne t\)。
\(1\le n\le 10000,1\le m\le 15000\)。
solution
vp 时写了个强行点分治寻找,写了 \(8\) KB 并顶着 \(O(\log^2 n)\) 的次数和优秀的常数把树的测试点全过了获得了 \(80\) 的高分,赛后才知道复杂度是假的。
考虑将图定向为一个 DAG,若在这个 DAG 上 \(s\) 能够到达 \(t\),那么可以通过二分在 \(O(\log n)\) 的次数内确定 \(s\) 和 \(t\)。
具体地,考虑拓扑排序,对于一个区间 \([l,r]\),若将两端点的拓扑序不都在 \([l,r]\) 内的边全部反转,则 \(s\) 仍然可以到达 \(t\),当且仅当 \(s,t\in [l,r]\),因为 \(s\) 拓扑序在 \(t\) 之前,且 \(s\) 到 \(t\) 的路径经过点的拓扑序一定在它们拓扑序之间,所以反转后 \(s\) 到 \(t\) 的路径不受影响,又因为若 \(s\) 不在 \([l,r]\) 中,则它指向拓扑序在它之后的点的边必然反转,此时 \(s\) 不再能够到达 \(t\),\(t\) 不在 \([l,r]\) 中也同理,所以这个条件是充要的。
那么就可以根据拓扑序从小到大,二分一个最短前缀满足反转两端点的拓扑序不都在这段前缀内的边后 \(s\) 仍然可以到达 \(t\),那么 \(t\) 的拓扑序就是这段前缀的最后一个值,同理在这段前缀上二分一个后缀即可得到 \(s\)。
不难发现可以将问题从图上转化到树上,取图中一棵生成树,调整树边方向,由于树的定向一定是 DAG,而非树边只需要让拓扑序小的点指向大的点即可,这样一定能够定向成一张 DAG,所以只需要考虑树的情况。
比较常规的思路是点分治,然后对于子树进行二进制分组,以分治中心为根,一组全部设为内向树另一组设为外向树,若所有分组中均不存在 \(s\) 可以到达 \(t\) 的情况则 \(s\) 到 \(t\) 的路径一定不经过分治中心,将分治中心删去后对子树继续该过程即可,这么做每层二进制分组是 \(O(\log n)\) 的,总询问次数 \(O(\log^2 n)\),不可接受。
但是还有另一种点分治方式,令当前分治子树大小为 \(siz\),考虑将分治中心的若干棵子树分成两组,则一定存在一种分配方式,使得两组内子树大小之和均在 \([\frac{1}{3} \times siz,\frac{2}{3} \times siz]\),按子树大小从小到大排序后,依次将子树加到一组中,直到这组子树大小超过 \(\frac{1}{3}\times siz\) 就停止,其余子树分到另一组即可。
根据这种分治方法,将分出的两组子树一组设为内向,一组设为外向,尝试是否存在 \(s\) 到 \(t\) 的路径,然后再将原先设为内向的组设为外向,另一组设为内向,再次尝试,若两次尝试都不能找到 \(s\) 到 \(t\) 的路径,则对这两组分别继续分治,分治中心在两组中同时保留,不能删除,由于 \(s\) 到 \(t\) 的路径一定不可能跨过这两个组别,所以路径一定在其中一组对应的连通块中,同时由于两个连通块只共点不共边,所以不会互相影响。
这样每层只需要进行两次询问,总次数是 \(O(\log n)\) 的,可以通过。
Code
#include "thief.h"
#include<cstdio>
#include<algorithm>
#include<vector>
#include<map>
#include<bitset>
#include<queue>
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;
int ans_st,ans_ed,_N;
vector<int> q;
int _U[N],_V[N];
map<int,int> mp[N];
vector<int> e[N];
inline void set_Edge(int u,int v) {
if (!u||!v) return;
int id=mp[u][v];
if (u==_U[id]&&v==_V[id]) q[id]=0;
else q[id]=1;
}
int in[N],dp[N],rk[N];
struct Edge{int v,id;};
vector<Edge> T[N];
bitset<N> did,dfn;
vector<int> G[N];
inline void topu() {
queue<int> q;
vector<int> id;
f(i,1,_N) if (!in[i]) q.push(i);
int idx=0;
while (!q.empty()) {
int now=q.front();
dp[now]=++idx;
rk[idx]=now;
q.pop();
for (int v:G[now]) {
in[v]--;
if (!in[v]) q.push(v);
}
}
f(i,1,_N) in[i]=0,G[i].clear();
}
inline void solve() {
int m=q.size();
f(i,0,m-1) {
int u=_U[i],v=_V[i];
if (!q[i]) G[u].push_back(v),in[v]++;
else G[v].push_back(u),in[u]++;
}
topu();
int l=1,r=_N,res=-1;
while (l<=r) {
int mid=l+r>>1;
f(i,0,m-1) {
int u=_U[i],v=_V[i];
if (dp[u]>mid||dp[v]>mid) q[i]^=1;
}
int ret=query(q);
f(i,0,m-1) {
int u=_U[i],v=_V[i];
if (dp[u]>mid||dp[v]>mid) q[i]^=1;
}
if (ret) res=mid,r=mid-1;
else l=mid+1;
}
ans_ed=rk[res];
l=1,r=res-1;res=-1;
while (l<=r) {
int mid=l+r>>1;
f(i,0,m-1) {
int u=_U[i],v=_V[i];
if (dp[u]<mid||dp[v]<mid) q[i]^=1;
}
int ret=query(q);
f(i,0,m-1) {
int u=_U[i],v=_V[i];
if (dp[u]<mid||dp[v]<mid) q[i]^=1;
}
if (ret) res=mid,l=mid+1;
else r=mid-1;
}
ans_st=rk[res];
}
inline int ask() {
if (ans_st&&ans_ed) return 0;
int m=q.size(),flag=0;
f(i,0,m-1) {
if (!did[i]) continue;
int u=_U[i],v=_V[i];
if (!q[i]) G[u].push_back(v),in[v]++;
else G[v].push_back(u),in[u]++;
}
topu();
f(i,0,m-1) {
if (did[i]) continue;
flag=1;
int u=_U[i],v=_V[i];
if (dp[u]<dp[v]) q[i]=0;
else q[i]=1;
}
if (query(q)) return solve(),1;
return 0;
}
bitset<N> vis;
int siz[N],wgh[N],rt,sum;
inline void get_rt(int now,int fa) {
siz[now]=1;wgh[now]=0;
for (int v:e[now]) {
if (vis[v]||v==fa) continue;
get_rt(v,now);
siz[now]+=siz[v];
wgh[now]=max(wgh[now],siz[v]);
}
wgh[now]=max(wgh[now],sum-siz[now]);
if (wgh[now]<wgh[rt]) rt=now;
}
inline void add1(int now,int fa) {
set_Edge(fa,now);
for (int v:e[now]) if (v!=fa&&!vis[v]) add1(v,now);
}
inline void add2(int now,int fa) {
set_Edge(now,fa);
for (int v:e[now]) if (v!=fa&&!vis[v]) add2(v,now);
}
vector<int> _sub;
inline void get_sub(int now,int fa) {
_sub.push_back(now);
for (int v:e[now]) if (v!=fa&&!vis[v]) get_sub(v,now);
}
inline void solve(vector<vector<int>> tmp) {
if (tmp.empty()) return;
vector<vector<int>> nxt;
for (vector<int> &vec:tmp) {
rt=0;sum=vec.size();
for (int x:vec) vis.reset(x);
get_rt(vec.back(),0);
int now=0,flag=0;
vector<int> v1,v2,id;
for (int v:e[rt]) if (!vis[v]) id.push_back(v);
sort(id.begin(),id.end(),[](int x,int y){return siz[x]<siz[y];});
// if (siz[id.back()]>sum/3) reverse(id.begin(),id.end());
for (int v:id) {
if (!flag) {
now+=siz[v];add1(v,rt);
get_sub(v,rt);
for (int x:_sub) v1.push_back(x);
_sub.clear();
if (now>=sum/3) flag=1;
}
else {
add2(v,rt);
get_sub(v,rt);
for (int x:_sub) v2.push_back(x);
_sub.clear();
}
}
for (int x:vec) vis.set(x);
if (!v1.empty()) v1.push_back(rt),nxt.push_back(v1);
if (!v2.empty()) v2.push_back(rt),nxt.push_back(v2);
}
if (ask()) return;
for (int &x:q) x^=1;
if (!ask()) solve(nxt);
}
inline void dfs_Tree(int now) {
if (ans_st&&ans_ed) return;
dfn.set(now);
for (Edge E:T[now]) {
int v=E.v,id=E.id;
if (!dfn[v]) dfs_Tree(v),did.set(id);
}
}
void solve(int N, int M, std::vector<int> U, std::vector<int> V) {
vis.set();q.resize(M);_N=N;
f(i,0,M-1) {
U[i]++,V[i]++;
_U[i]=U[i];_V[i]=V[i];
T[U[i]].push_back({V[i],i});
T[V[i]].push_back({U[i],i});
}
dfs_Tree(1);
f(i,0,M-1) {
if (!did[i]) continue;
_U[i]=U[i];_V[i]=V[i];
mp[U[i]][V[i]]=i,mp[V[i]][U[i]]=i;
e[U[i]].push_back(V[i]);
e[V[i]].push_back(U[i]);
}
wgh[0]=inf;
vector<int> id;
f(i,1,N) id.push_back(i);
vector<vector<int>> st;
st.push_back(id);solve(st);
answer(ans_st-1,ans_ed-1);
}

浙公网安备 33010602011771号