2024 Noip 做题记录(八)
\(\text{By DaiRuiChen007}\)
Round #29 - 2024.11.6
A. Add One 2
题目大意
给定 \(y_1\sim y_n\),初始 \(x_1\sim x_n\) 全为 \(0\),每次可以花 \(k\) 的代价给一个长度为 \(k\) 的前缀或后缀 \(+1\),求让所有 \(x_i\ge y_i\) 的最小代价。
数据范围:\(n\le 10^6\)。
思路分析
考虑怎样的 \(x\) 可以构造,只需要最小化 \(\sum x_i\)。
观察差分,我们发现如果 \(x_i>x_{i+1}\),那么操作前缀 \([1,i]\) 至少 \(x_{i}-x_{i+1}\) 次,否则操作后缀 \([i+1,n]\) 至少 \(x_{i+1}-x_i\) 次。
将这些操作撤销能够得到 \(x_i\) 全相等,只要 \(x_1\ge 0\) 即可,要求 \(x_1\ge \sum_{i=1}^{n-1}\max(0,x_i-x_{i+1})\)。
为了方便,在 \(x_0\) 和 \(x_{n+1}\) 处添加两个足够大的元素 \(X\),那么就变成要求 \(x_0\ge \sum_{i=0}^n\max(0,x_i-x_{i+1})\)。
考虑把 \(x_{n+1}\) 的限制也加入,变成:
即 \(\sum |x_i-x_{i+1}|\le 2X\),我们要从 \(\{y_i\}\) 开始,给若干元素增加,使得 \(\sum |y_i-y_{i+1}|\le 2X\)。
可以发现每次找到一段连续的局部最小值(\(a_{i-1}>a_i<a_{i+1}\))加一,使得限制 \(\sum |y_i-y_{i+1}|\) 减二。
此时每次操作的区间都是笛卡尔树上的一个子树,按子树大小从小到大贪心即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,inf=1e9+7;
int n,l[MAXN],r[MAXN],a[MAXN],st[MAXN],tp;
void solve() {
cin>>n,a[0]=a[n+1]=inf;
for(int i=1;i<=n;++i) cin>>a[i];
st[tp=0]=0;
for(int i=1;i<=n;++i) {
while(a[st[tp]]<a[i]) --tp;
l[i]=st[tp],st[++tp]=i;
}
st[tp=0]=n+1;
for(int i=n;i>=1;--i) {
while(a[st[tp]]<=a[i]) --tp;
r[i]=st[tp],st[++tp]=i;
}
vector <array<int,2>> q;
for(int i=1;i<=n;++i) if(a[i]<a[l[i]]&&a[i]<a[r[i]]) {
q.push_back({r[i]-l[i]-1,min(a[l[i]],a[r[i]])-a[i]});
}
ll w=-2*inf,s=accumulate(a+1,a+n+1,0ll);
for(int i=0;i<=n;++i) w+=abs(a[i+1]-a[i]);
sort(q.begin(),q.end()),w>>=1;
for(auto i:q) {
if(w<=0) break;
s+=min(w,(ll)i[1])*i[0],w=max(0ll,w-i[1]);
}
cout<<s<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
K. Sticks
题目大意
定义一个 \(n\times n\) 的 01 网格,一次操作可以把某一行或某一列的全 \(0\) 前缀变成 \(1\)。
如果一个网格是好的,当且仅当其可以从全 \(0\) 网格进行若干次操作得到。
给定一个带
?的 01 网格,求有多少种给?填 01 的方法使得其是好的。数据范围:\(n\le 3000\)。
思路分析
题目限制相当于每行每列覆盖一个前缀 \(r_i,c_j\),且覆盖范围不相交。
对于每种覆盖方式计数更为简单,考虑把每个好的网格唯一对应到一种覆盖方式上。
这就是 AGC035F,因此唯一的要求是不存在 \(r_i=j-1,c_j=i\),即两个前缀恰好都在 \((i,j)\) 处结束,钦定其被第 \(i\) 行的前缀覆盖。
那么可以 dp,\(f_{i,j}\) 表示前 \(i\) 行最大的 \(r_i=j\),转移时考虑当前这一行 \(r_i\) 是否是最大的,然后动态维护每一列的选择方案数。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3005,MOD=998244353;
char s[MAXN][MAXN];
ll f[MAXN],vc[MAXN];
bool okc[MAXN];
signed main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%s",s[i]+1),s[i][0]=s[0][i]='1',vc[i]=okc[i]=1;
f[0]=vc[0]=okc[0]=1;
for(int i=1;i<=n;++i){
ll sf=0,vr=0;
bool okr=1;
for(int j=0;j<=n;++j) {
if(s[i][j]=='0') okr=0;
else if(s[i][j]=='1') vr=okr;
else vr+=okr;
ll w=f[j]*vr%MOD;
if(okr) w=(w+sf*(vc[j]-okc[j]))%MOD;
sf=(sf*vc[j]+f[j])%MOD,f[j]=w;
}
for(int j=0;j<=n;++j) {
if(s[i][j]=='0') okc[j]=0;
else if(s[i][j]=='1') vc[j]=okc[j];
else vc[j]+=okc[j];
}
}
ll ans=0;
for(int j=0;j<=n;++j) ans=(ans*vc[j]+f[j])%MOD;
printf("%lld\n",ans);
return 0;
}
M. Bot Friends
题目大意
给定 \(0\sim n\) 数轴,第 \(i\) 个球在 \(i+0.5\) 上,\(0\sim n\) 上有洞,你需要以某种顺序依次推每个球。
每个球会沿其指定方向运动(有些球指定方向可以自由确定),进入第一个没有球的洞中。
一个球是好的当且仅当其进洞且不在左右相邻的洞中。
数据范围:\(n\le 5000\)。
思路分析
观察每个球和洞的匹配,其合法当且仅当这些匹配对应的线段在数轴上两两包含或相交。
那么可以做一个形似括号序列的 dp,\(f_{i,j}/g_{i,j}\) 表示前 \(i\) 个球和洞,当前剩下 \(j\) 个洞向外部匹配,并且第 \(i\) 个球的方向是左 / 右。
转移的时候考虑这个点的匹配形式,需要考虑把这个点前面的洞预留出来给后面的球的转移。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005,inf=1e9;
char s[MAXN];
int n,f[MAXN][MAXN],g[MAXN][MAXN];
inline void chkmax(int &x,int y) { x=x>y?x:y; }
void solve() {
scanf("%s",s+1),n=strlen(s+1);
for(int i=0;i<=n;++i) for(int j=0;j<=n;++j) f[i][j]=g[i][j]=-inf;
f[0][0]=0;
for(int i=1;i<=n;++i) {
if(s[i]!='>') chkmax(f[i][0],f[i-1][0]);
if(s[i]!='<') chkmax(g[i][1],f[i-1][0]);
for(int j=1;j<i;++j) {
if(s[i]!='>') {
chkmax(f[i][j-1],max(f[i-1][j]+2,g[i-1][j]+1));
chkmax(f[i][j],g[i-1][j]);
}
if(s[i]!='<') {
chkmax(g[i][j],f[i-1][j]+1);
chkmax(g[i][j+1],max(f[i-1][j],g[i-1][j]));
}
}
}
int ans=0;
for(int j=0;j<=n;++j) chkmax(ans,max(f[n][j]+(j>0),g[n][j]));
printf("%d\n",ans);
}
signed main() {
int T; scanf("%d",&T);
while(T--) solve();
return 0;
}
*L. Exchanging Kubic
题目大意
交互器有序列 \(a_1\sim a_n\),你可以询问 \(2n\) 次某个区间的最大子段和,构造出一个 \(b_1\sim b_n\) 使得 \(a\) 与 \(b\) 每个区间的最大子段和都相等。
数据范围:\(n\le 1000\)。
思路分析
询问所有 \([i,i]\) 可以得到 \(\max(a_i,0)\),那么 \(>0\) 的元素全部确定,并且符号相同的元素可以缩成连续的一段(认为符号为 \([a_i>0]\))。
那么这个序列会变成正负交错的一个序列,选取其中的一个 \(+-+\) 子区间 \(a_{i-1},a_i,a_{i+1}\) 并询问。
如果得到的值不是 \(\max(a_{i-1},a_{i+1})\),那么我们就知道了 \(a_{i-1}+a_i+a_{i+1}\),从而确定 \(a_i\),并且最大子段和不可能单独过这个区间的前缀或后缀,因此可以把这三个元素缩起来。
否则我们知道 \(|a_i|\ge \min(a_{i-1},a_{i+1})\),考虑如何利用这个性质。
为了把 \(\min\) 的影响去除,我们找到最小的 \(>0\) 的 \(a_i\),然后询问 \(a_{i-2}\sim a_i\) 以及 \(a_{i}\sim a_{i+2}\)。
如果这两个询问中有一个能确定 \(a_{i-1}/a_{i+1}\),直接把对应的区间缩起来。
否则我们知道 \(|a_{i-1}|\ge a_i,|a_{i+1}|\ge a_i\),因此对于子区间 \(a_{i-1},a_{i},a_{i+1}\),最大子段和不可能单独过这个区间的前缀或后缀。
因此我们可以把这三个元素也缩起来,并且把 \(a_{i-1}\) 和 \(a_{i+1}\) 中的一个赋值为 \(-a_{i}\),剩余的一个取合成出的新的元素构造出来的权值。
容易发现 \(2\) 次操作能把三个元素缩成一个,加上初始确定符号的 \(n\) 次操作,操作次数 \(2n\)。
时间复杂度 \(\mathcal O(n^2)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2005;
struct seg { int l,r; ll s; };
int n;
ll a[MAXN];
ll qry(int l,int r) {
cout<<"? "<<l<<" "<<r<<endl;
ll z; cin>>z; return z;
}
void solve() {
cin>>n;
vector <seg> p;
for(int i=1;i<=n;++i) {
a[i]=qry(i,i);
if(p.size()&&(p.back().s>0)==(a[i]>0)) p.back().s+=a[i],p.back().r=i;
else p.push_back({i,i,a[i]});
}
if(!p.front().s) p.erase(p.begin());
if(!p.back().s) p.erase(--p.end());
auto pop=[&](int l,int r) { p.erase(p.begin()+l,p.begin()+r+1); };
while(p.size()>1) {
int i=0;
for(int j=0;j<(int)p.size();j+=2) if(p[j].s<p[i].s) i=j;
if(i-2>=0) {
ll x=qry(p[i-2].l,p[i].r);
if(x>p[i-2].s) {
a[p[i-1].l]=x-p[i-2].s-p[i].s;
p[i-2].r=p[i].r,p[i-2].s=x,pop(i-1,i);
continue;
}
}
if(i+2<(int)p.size()) {
ll x=qry(p[i].l,p[i+2].r);
if(x>p[i+2].s) {
a[p[i+1].l]=x-p[i+2].s-p[i].s;
p[i+2].l=p[i].l,p[i+2].s=x,pop(i,i+1);
continue;
}
}
if(!i) a[p[i+1].l]=-p[i].s,pop(i,i+1);
else if(i+1==(int)p.size()) a[p[i-1].l]=-p[i].s,pop(i-1,i);
else a[p[i+1].l]=-p[i].s,p[i-1].r=p[i+1].r,pop(i,i+1);
}
cout<<"! "; for(int i=1;i<=n;++i) cout<<a[i]<<" "; cout<<endl;
}
signed main() {
int T; cin>>T;
while(T--) solve();
return 0;
}
*B. Periodic Sequence
题目大意
给定 \(n\),对于 \(k=1\sim n\),求出最长的周期字符串序列,满足其中的元素两两不同,且长度 \(\le k\)。
一个字符串序列 \(s_1\sim s_k\) 是周期字符串序列,当且仅当所有 \(s_i\) 都是 \(s_{i+1}\) 的周期。
特别地:如果 \(y\) 是 \(x\) 的前缀,那么 \(x\) 也是 \(y\) 的一个周期。
数据范围:\(n\le 2\times 10^5\)。
思路分析
先刻画 \(k=n\) 的答案。
首先 \(|s_1|<n\) 的话,可以在开头插入一个长度为 \(n\) 且以 \(s_1\) 为前缀的字符串。
因此最优解一定有 \(|s_1|=n\)。
通过归纳法可以发现这个序列中的字符串一定是由 \(s_1\) 的若干前缀拼成的,且第一个前缀是最长的。
对于每个这样方法构造出的字符串,我们可以把他们都安排到一个序列中,而这就是答案。
具体构造时先把所有字符串插入 Trie,每个节点只有两个儿子,一个是 \(s_1\) 中当前位置的下一个字符,另一个是 \(s_1\) 的开头。
优先遍历 \(s_1\) 中当前位置的下一个字符,取出 Trie 的中序遍历即为一组合法的序列。
具体可以取 \(s_1=\texttt{abbbb}\dots \texttt b\)。
那么我们就要对每个 \(n\) 计数 \(\sum_{i=1}^m a_i\le n\) 且 \(a_1=\max a_i\) 的序列个数。
枚举 \(a_1=k\) 得到答案的生成函数为:
其中求和号外面的 \(\dfrac{1}{1-x}\) 是把长度恰 \(=n\) 的序列个数做前缀和。
化简得到:
我们要求 \(F(x)\) 的 \(1\sim n\) 次项系数,显然只有 \(k\le n\) 的项有用。
对于每个 \(k\),用类似背包的方法求 \(\sum_t(2x-x^{k+1})^t\),即选一个大小为 \(1\) 的物品权值为 \(2\),选一个大小为 \(k+1\) 的物品权值为 \(-1\),复杂度 \(\mathcal O(n^2)\)。
对于 \(k\) 比较大的情况,考虑另一种方法:
很显然后面的和式在 \(\le n\) 次项处至多 \(\mathcal O(n/k)\) 项。
从大到小枚举每一项 \(i\),对所有 \(k\) 一起处理,用类似秦九韶算法的方式,给每个 \(x^{k+i(k+1)}\) 加上 \((-1)^{i}\),再给整个生成函数乘以 \(\dfrac{1}{1-2x}\)。
这些过程都可以 \(\mathcal O(n)\) 处理,总复杂度 \(\mathcal O(n^2/k)\)。
对 \(k\) 根号分治即可。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,MOD;
ll dp[MAXN],f[MAXN],g[MAXN];
void dp1(int k) {
memset(f,0,sizeof(f));
f[k]=1;
for(int i=k+1;i<=n;++i) f[i]=(2*f[i-1]+MOD-f[i-k-1])%MOD;
for(int i=1;i<=n;++i) dp[i]=(dp[i]+f[i])%MOD;
}
signed main() {
scanf("%d%d",&n,&MOD);
int B=sqrt(n);
for(int i=1;i<=B;++i) dp1(i);
for(int x=B;x>=0;--x) {
for(int k=B+1;k<=n;++k) {
int i=(k+1)*x+k;
if(i<=n) g[i]=(g[i]+(x&1?MOD-1:1))%MOD;
}
for(int i=1;i<=n;++i) g[i]=(g[i]+2*g[i-1])%MOD;
}
for(int i=1;i<=n;++i) dp[i]=(dp[i]+g[i])%MOD;
for(int i=1;i<=n;++i) printf("%lld ",dp[i]); puts("");
return 0;
}
*E. Min or Max 2
题目大意
给定两个 \(1\sim n\) 排列 \(a,b\),初始 \((x,y)=(a_1,b_1)\),对于 \(i=2\sim n\),依次选择把 \((x,y)\) 变成 \((\min(x,a_i),\min(y,b_i))\) 或 \((\max(x,a_i),\max(y,b_i))\)。
数据范围:\(n\le 5\times 10^5\)。
思路分析
可以证明对于每个所有最终合法的 \((x,y)\),按 \(x+y\) 排序后,其后继一定是 \((x+1,y)/(x,y+1)/(x+1,y+1)\)。
因此每个 \(x\) 对应合法的 \(y\) 是一个区间,且区间是单调的。
考虑求最大可能的 \(y\),那么这个右端点随 \(x\) 递增递增,可以双指针维护,只要判定是否存在一组 \(i\le x,j\ge y\) 的合法对 \((i,j)\)。
分析发现我们只关心每对 \((i,j)\) 的 \([i\le x]+[j\ge y]\),此时贪心地最大化这个值一定是最优的。
那么每个位置对应一个大小为 \(3\) 的映射,表示每个状态会转移到哪里,线段树维护区间映射积。
我们发现增加 \(x\) 只会改变 \(a_i=x\) 的位置上的映射,增加 \(y\) 同理,那么双指针过程中只会单点修改 \(\mathcal O(n)\) 个映射。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
typedef array<int,3> info;
const int MAXN=5e5+5;
const info I[3]={{0,0,1},{0,1,1},{1,1,2}};
int n,X,Y,a[MAXN],b[MAXN];
inline info operator +(const info &u,const info &v) { return {v[u[0]],v[u[1]],v[u[2]]}; }
struct SegmentTree {
static const int N=1<<19;
info tr[N<<1];
void init() { fill(tr,tr+(N<<1),info{0,1,2}); }
void upd(int x,info o) {
for(tr[x+=N]=o,x>>=1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
}
} T;
int pa[MAXN],pb[MAXN],mx[MAXN];
void sol() {
X=0,Y=1;
for(int i=1;i<=n;++i) pa[a[i]]=pb[b[i]]=i;
auto ins=[&](int i) { if(i>1) T.upd(i,I[(a[i]>X)+(b[i]<Y)]); };
for(int i=2;i<=n;++i) ins(i);
for(X=1;X<=n;++X) {
ins(pa[X]);
while(Y<=n&&!T.tr[1][(a[1]>X)+(b[1]<Y)]) ++Y,ins(pb[Y-1]);
mx[X]=Y-1;
}
for(int i=2;i<=n;++i) T.upd(i,{0,1,2});
}
int l[MAXN],r[MAXN],f[MAXN*2];
void solve() {
cin>>n,fill(f,f+2*n+1,0);
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=n;++i) cin>>b[i];
sol();
for(int i=1;i<=n;++i) r[i]=mx[i],a[i]=n-a[i]+1,b[i]=n-b[i]+1;
sol();
for(int i=1;i<=n;++i) l[n-i+1]=n-mx[i]+1;
for(int i=1;i<=n;++i) {
++f[l[i]+n-i],--f[r[i]+n-i+1];
}
for(int i=1;i<=2*n;++i) f[i]+=f[i-1];
cout<<f[n]<<" ";
for(int i=1;i<n;++i) cout<<f[n+i]+f[n-i]<<" "; cout<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int _; cin>>_,T.init();
while(_--) solve();
return 0;
}
Round #30 - 2024.11.12
Contest Link: 2024 ICPC Kunming Invitational Contest
E. Relearn through Review
题目大意
给定 \(a_1\sim a_n\),选定其中一个区间 \([l,r]\),给 \(a_l\sim a_r\) 加上 \(k\),最大化 \(\gcd(a_i)\)。
数据范围:\(n\le 3\times 10^5,a_i,k\le 10^{18}\)。
思路分析
设 \(w(l,r)=\gcd_{i=l}^r\{a_i+k\}\),那么如果 \(w(l,r)=w(l-1,r)\),则选择 \([l,r]\) 作为区间肯定不优。
因此我们从小到大枚举 \(l\),\(l-1\to l\) 的过程中,\(w(l,r)\) 会变化的 \(r\) 一定是一段前缀,并且均摊只有 \(\mathcal O(n\log V)\) 个 \((l,r)\) 满足 \(w(l,r)\ne w(l-1,r)\)。
因此直接枚举满足条件的 \(l,r\) 并计算答案即可。
时间复杂度 \(\mathcal O(n\log^2V)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
ll gcd(ll x,ll y) {
if(!x||!y) return x|y;
return gcd(y,x%y);
}
ll a[MAXN],k,pre[MAXN],suf[MAXN],val[MAXN];
void solve() {
int n;
cin>>n>>k;
for(int i=1;i<=n;++i) cin>>a[i];
pre[0]=suf[n+1]=val[0]=0;
for(int i=1;i<=n;++i) pre[i]=gcd(pre[i-1],a[i]),val[i]=gcd(val[i-1],a[i]+k);
for(int i=n;i>=1;--i) suf[i]=gcd(suf[i+1],a[i]);
ll ans=pre[n];
for(int i=1;i<=n;++i) ans=max(ans,gcd(val[i],suf[i+1]));
for(int l=2;l<=n;++l) {
val[l-1]=0;
for(int r=l;r<=n;++r) {
ll z=gcd(val[r-1],a[r]+k);
if(z==val[r]) break;
val[r]=z,ans=max(ans,gcd(z,gcd(pre[l-1],suf[r+1])));
}
}
cout<<ans<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
J. The Quest for El Dorado
题目大意
给定 \(n\) 个点 \(m\) 条带权边的无向图,每条边有颜色,从 \(1\) 出发顺次进行 \(k\) 步操作,第 \(i\) 次沿 \(c_i\) 颜色的边走 \(\le d_i\) 的距离,求最终能到达哪些点。
数据范围:\(n,m,k\le 5\times 10^5\)。
思路分析
考虑对每种颜色的图维护一个 Dijkstra,取出到当前点集距离 \(\le d_i\) 的所有点,然后再用这些点更新其他点的最短距离。
时间复杂度 \(\mathcal O(m\log m)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5;
const ll inf=1e18;
int n,E,m;
priority_queue <array<ll,2>,vector<array<ll,2>>,greater<array<ll,2>>> Q[MAXN];
ll dis[MAXN<<1];
struct Edge { int v,w; };
vector <Edge> G[MAXN<<1];
int tot=0,ver[MAXN<<1];
map <int,int> id[MAXN];
int ID(int u,int c) {
if(!id[u].count(c)) {
id[u][c]=++tot;
ver[tot]=u;
}
return id[u][c];
}
void ins(int u) {
for(auto it:id[u]) {
int c=it.first,z=it.second;
dis[z]=0;
for(auto e:G[z]) {
if(dis[e.v]>e.w) {
Q[c].push({dis[e.v]=e.w,e.v});
}
}
}
}
bool ans[MAXN];
void solve() {
cin>>n>>E>>m;
for(int i=1,u,v,c,w;i<=E;++i) {
cin>>u>>v>>c>>w;
int x=ID(u,c),y=ID(v,c);
G[x].push_back({y,w});
G[y].push_back({x,w});
}
ins(1),ans[1]=true;
for(int c,lim;m--;) {
cin>>c>>lim;
vector <int> cur;
while(Q[c].size()&&Q[c].top()[0]<=lim) {
int i=Q[c].top()[1]; Q[c].pop();
if(ans[ver[i]]) continue;
ans[ver[i]]=true,cur.push_back(ver[i]);
for(auto e:G[i]) if(dis[e.v]>dis[i]+e.w) {
Q[c].push({dis[e.v]=dis[i]+e.w,e.v});
}
}
for(int u:cur) ins(u);
}
for(int i=1;i<=n;++i) cout<<ans[i];
cout<<"\n";
for(int i=1;i<=n;++i) id[i].clear(),ans[i]=0;
for(int i=1;i<=tot;++i) G[i].clear(),dis[i]=inf;
for(int i=1;i<=E;++i) while(Q[i].size()) Q[i].pop();
tot=0;
return ;
}
signed main() {
ios::sync_with_stdio(false);
fill(dis,dis+(MAXN<<1),inf);
int T; cin>>T;
while(T--) solve();
return 0;
}
L. Trails
题目大意
给定 \(p\times q\) 网格图,\(n\) 条额外边连接 \((x_i,y_i)\to (x_i+1,y_i+1)\),求 \((0,0)\) 到每个点的距离之和。
数据范围:\(n,p,q\le 10^6\)。
思路分析
首先一个点 \((i,j)\) 的最短路是 \(i+j\) 减去 \((0,0)\) 到 \((i,j)\) 的最长二维上升路径长度。
即找到最多的 \((x_{p_1},y_{p_1})\sim (x_{p_t},y_{p_t})\) 使得两维分别严格递增。
那么先加上所有点的 \(i+j\),然后减去每个点的最长二维上升路径长度。
先求出到每个 \((x_i,y_i)\) 的最长二维上升路径长度 \(f_i\),然后计算路径长度 \(\ge k\) 的点数,即取出 \(f_i\ge k\) 的所有点,将 \((x_i,p]\times (y_i,q]\) 求矩形并面积。
显然 \(f_i>k\) 的点的矩形被 \(f_i=k\) 的矩形直接包含,对 \(f_i=k\) 的点排序即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5;
struct FenwickTree {
int tr[MAXN]; vector <int> e;
void upd(int x,int v) { for(e.push_back(x);x<MAXN;x+=x&-x) tr[x]=max(tr[x],v); }
int qry(int x) { int s=0; for(;x;x&=x-1) s=max(s,tr[x]); return s; }
void init() { for(int x:e) for(;x<MAXN;x+=x&-x) tr[x]=0; e.clear(); }
} T;
int n,p,q,dp[MAXN];
struct poi { int x,y; } a[MAXN];
vector <poi> f[MAXN];
void solve() {
cin>>n>>p>>q,T.init();
for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y;
sort(a+1,a+n+1,[&](auto u,auto v){ return u.x^v.x?u.x<v.x:u.y>v.y; });
int len=0;
for(int i=1;i<=n;++i) {
dp[i]=T.qry(a[i].y)+1,len=max(len,dp[i]);
if(a[i].x<p&&a[i].y<q) f[dp[i]].push_back({a[i].x+1,a[i].y+1});
T.upd(a[i].y+1,dp[i]);
}
ll ans=1ll*p*(p+1)/2*(q+1)+1ll*q*(q+1)/2*(p+1);
for(int x=1;x<=len;++x) {
f[x].push_back({p+1,0});
for(int i=0;i+1<(int)f[x].size();++i) {
ans-=1ll*(f[x][i+1].x-f[x][i].x)*(q-f[x][i].y+1);
}
f[x].clear();
}
cout<<ans<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int _; cin>>_;
while(_--) solve();
return 0;
}
F. Collect the Coins
题目大意
\(n\) 枚硬币,第 \(i\) 枚恰好在 \(t_i\) 时刻出现在 \(x_i\),有两个机器人要接住所有硬币,求最大速度的最小值。
数据范围:\(n\le 10^6\)。
思路分析
先二分速度 \(v\),然后转为判定两个机器人能否接住所有硬币。
用最小链覆盖刻画,如果 \(t_i<t_j\) 且 \(|x_i-x_j|\le v(t_j-t_i)\),那么连边 \(i\to j\),\(v\) 合法当且仅当该 DAG 上最小链覆盖 \(\le 2\)。
用 Dilworth 定理转成求最长反链,那么 \(i,j\) 无边当且仅当 \(|x_i-x_j|>v|t_j-t_i|\)。
-
如果 \(x_i\ge x_j\),那么 \(x_i-x_j>v(t_j-t_i)\) 且 \(x_i-x_j>v(t_i-t_j)\),化简得到 \(x_i+vt_i>x_j+vt_j,x_i-vt_i>x_j-vt_j\)。
-
如果 \(x_j<x_i\),那么条件变成 \(x_i+vt_i<x_j+vt_j,x_i-vt_i<x_j-vt_j\)。
只要上述条件满足其一,容易发现这相当于 \((x_i-vt_i,x_i+vt_i),(x_j-vt_j,x_j+vt_j)\) 构成严格二维偏序。
那么我们只需要检验这些点的 LIS 长度是否 \(\le 2\)。
按横坐标排序从小到大扫描,维护 LIS 长度为 \(1\) 和 \(2\) 的点的最小纵坐标。
时间复杂度 \(\mathcal O(n\log n\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,inf=1e9;
const ll INF=1e18;
int n,t[MAXN],x[MAXN];
struct poi { ll x,y; } a[MAXN];
bool f[MAXN];
bool chk(ll v) {
for(int i=1;i<=n;++i) a[i]={x[i]-1ll*v*t[i],x[i]+1ll*v*t[i]},f[i]=false;
sort(a+1,a+n+1,[&](auto i,auto j){ return i.x^j.x?i.x<j.x:i.y<j.y; });
ll Y0=INF,Y1=INF;
for(int i=1,j;i<=n;i=j) {
for(j=i;j<=n&&a[j].x==a[i].x;++j);
for(int k=i;k<j;++k) {
if(Y1<a[k].y) return false;
if(Y0<a[k].y) f[k]=true;
}
for(int k=i;k<j;++k) {
Y0=min(Y0,a[k].y);
if(f[k]) Y1=min(Y1,a[k].y);
}
}
return true;
}
void solve() {
cin>>n;
for(int i=1;i<=n;++i) cin>>t[i]>>x[i];
int l=0,r=inf,v=-1;
while(l<=r) {
int mid=(l+r)>>1;
if(chk(mid)) v=mid,r=mid-1;
else l=mid+1;
}
cout<<v<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
H. Subarray
题目大意
给定 \(a_1\sim a_n\),对每个 \(k\) 求 \(a\) 中有多少子区间的最大值恰好出现了 \(k\) 次。
数据范围:\(n\le 4\times 10^5\)。
思路分析
考虑枚举最大值 \(x\),那么整个序列会被 \(>x\) 的元素分成若干段,把每段提取出来,可以看成一个 01 序列,要求 1 出现了 \(k\) 次的子序列个数。
假设这个 01 序列是 \(b_1\sim b_m\),一种实现方式是计算 \(b[1,l)\) 和 \(b(r,m]\) 中 1 的个数。
即 \(L_k\) 表示多少个 \(l\) 使得 \(b[1,l)\) 中有 \(k\) 个 \(1\),\(R_k\) 表示多少个 \(r\) 使得 \(b(r,m]\) 中有 \(k\) 个 \(1\),对这两个函数卷积即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
namespace P {
const int MOD=998244353,N=1<<20,G=3;
int rev[N],inv[N],fac[N],ifac[N],w[N<<1];
int ksm(int a,int b=MOD-2) {
int ret=1;
for(;b;a=1ll*a*a%MOD,b=b>>1) if(b&1) ret=1ll*ret*a%MOD;
return ret;
}
void poly_init() {
inv[1]=1;
for(int i=2;i<N;++i) inv[i]=1ll*(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[0]=ifac[0]=1;
for(int i=1;i<N;++i) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*inv[i]%MOD;
for(int k=1;k<=N;k<<=1) {
int x=ksm(G,(MOD-1)/k); w[k]=1;
for(int i=1;i<k;++i) w[i+k]=1ll*x*w[i+k-1]%MOD;
}
}
int plen(int x) { int y=1; for(;y<x;y<<=1); return y; }
void ntt(int *f,bool idft,int n) {
for(int i=0;i<n;++i) {
rev[i]=(rev[i>>1]>>1);
if(i&1) rev[i]|=n>>1;
}
for(int i=0;i<n;++i) if(rev[i]<i) swap(f[i],f[rev[i]]);
for(int k=2,x,y;k<=n;k<<=1) {
for(int i=0;i<n;i+=k) {
for(int j=i;j<i+k/2;++j) {
x=f[j],y=1ll*f[j+k/2]*w[k+j-i]%MOD;
f[j]=(x+y>=MOD)?x+y-MOD:x+y,f[j+k/2]=(x>=y)?x-y:x+MOD-y;
}
}
}
if(idft) {
reverse(f+1,f+n);
for(int i=0,x=ksm(n);i<n;++i) f[i]=1ll*f[i]*x%MOD;
}
}
void poly_mul(const int *f,const int *g,int *h,int n,int m) {
static int a[N],b[N];
for(int i=0;i<n;++i) a[i]=f[i];
for(int i=0;i<m;++i) b[i]=g[i];
int len=plen(n+m-1);
ntt(a,0,len),ntt(b,0,len);
for(int i=0;i<len;++i) h[i]=1ll*a[i]*b[i]%MOD;
ntt(h,1,len);
memset(a,0,sizeof(int)*len);
memset(b,0,sizeof(int)*len);
}
}
const int MAXN=4e5+5,N=1<<20,MOD=998244353;
int n,a[MAXN],st[MAXN],tp,L[MAXN],R[MAXN],nxt[MAXN];
int m,b[MAXN],X[N],Y[N],Z[N],f[MAXN];
void calc() {
for(int i=0;i<=m;++i) {
X[i]=b[i+1]-b[i]-(i==m);
Y[i]=b[m+1-i]-b[m-i]-(i==m);
}
P::poly_mul(X,Y,Z,m+1,m+1);
for(int i=0;i<m;++i) f[m-i]=(f[m-i]+Z[i])%MOD;
}
void solve() {
cin>>n;
map <int,int> pos;
for(int i=1;i<=n;++i) cin>>a[i],f[i]=0;
for(int i=n;i>=1;--i) {
if(!pos.count(a[i])) nxt[i]=n+2;
else nxt[i]=pos[a[i]];
pos[a[i]]=i;
}
for(int i=1;i<=n;++i) {
while(tp&&a[st[tp]]<a[i]) R[st[tp--]]=i;
st[++tp]=i;
}
while(tp) R[st[tp--]]=n+1;
for(int i=n;i>=1;--i) {
while(tp&&a[st[tp]]<a[i]) L[st[tp--]]=i;
st[++tp]=i;
}
while(tp) L[st[tp--]]=0;
for(auto it:pos) {
int u=it.second;
while(u<=n) {
int v=u; b[0]=L[u],m=0;
while(v<R[u]) b[++m]=v,v=nxt[v];
b[m+1]=R[u],calc(),u=v;
}
}
int ans=0;
for(int i=1;i<=n;++i) ans=(ans+1ll*f[i]*f[i]%MOD*i)%MOD;
cout<<ans<<"\n";
}
signed main() {
P::poly_init();
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
K. Permutation
题目大意
交互器有一个 \(n\) 阶排列 \(p\),每次可以询问一个序列 \(a_1\sim a_n\) 与 \(p\) 的海明距离,在 \(6666\) 次询问内还原 \(p\)。
数据范围:\(n\le 1000\)。
思路分析
考虑分治,每次只要解决每个元素是在序列的前一半还是后一半出现的问题即可。
对于每个元素,依次将其填满在前一半,可以用 \(n\) 次询问分治,总操作次数 \(n\log_2n\)。
需要一定的常数优化。
容易发现此时序列的后一半被浪费了,如果我们同时填两个元素,即 \(x\) 填前一半,\(y\) 填后一半。
-
那么当询问的答案为 \(0/2\) 时,可以直接确定 \(x,y\) 属于哪一半。
-
如果询问答案为 \(1\),说明 \(x,y\) 属于序列的同一半,那么把 \(x\) 和 \(y\) 看成同一个元素,相当于只确定 \(x\)。
每次询问前随机一个交互顺序,那么每次询问相邻两个元素,如果属于同一半,那么一次询问只能确定一个元素,否则能确定两个元素。
那么每次询问期望能确定 \(\dfrac 32\) 个元素,期望操作次数 $\dfrac 23n\log_2n $。
时间复杂度 \(\mathcal O(n^2\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005;
int n,a[MAXN],p[MAXN];
int qry() {
cout<<"0 "; for(int i=1;i<=n;++i) cout<<a[i]<<" "; cout<<endl;
int x; cin>>x; return x;
}
mt19937 rnd(time(0));
void solve(int l,int r,vector<int>&v) {
if(l==r) return p[l]=v[0],void();
int mid=(l+r)>>1;
shuffle(v.begin(),v.end(),rnd);
vector <int> lv,rv,st;
for(int x:v) {
if((int)lv.size()==mid-l+1) { rv.push_back(x); continue; }
if((int)rv.size()==r-mid) { lv.push_back(x); continue; }
if(st.empty()) { st.push_back(x); continue; }
fill(a+1,a+n+1,st[0]);
fill(a+l,a+mid+1,x);
int z=qry();
if(z==0) {
rv.push_back(x);
lv.insert(lv.end(),st.begin(),st.end());
st.clear();
} else if(z==2) {
lv.push_back(x);
rv.insert(rv.end(),st.begin(),st.end());
st.clear();
} else st.push_back(x);
}
if((int)lv.size()<mid-l+1) lv.insert(lv.end(),st.begin(),st.end());
else rv.insert(rv.end(),st.begin(),st.end());
solve(l,mid,lv),solve(mid+1,r,rv);
}
signed main() {
cin>>n;
vector <int> v;
for(int i=1;i<=n;++i) v.push_back(i);
solve(1,n,v);
cout<<"1 "; for(int i=1;i<=n;++i) cout<<p[i]<<" "; cout<<endl;
return 0;
}
C. Stop the Castle 2
题目大意
网格上有 \(n\) 个棋子和 \(m\) 个障碍,每对同行同列的棋子如果中间没有障碍就会互相攻击,现在删除 \(k\) 个障碍,构造方案最小化互相攻击的棋子对数。
数据范围:\(n,m\le 10^5\)。
思路分析
转成放置 \(m-k\) 个障碍,发现每个障碍会阻挡 \(0\sim 2\) 对棋子的攻击。
那么我们只要最大化能阻挡 \(2\) 对棋子攻击的障碍数量,这就是一个二分图最大匹配问题。
求出最大匹配后,如果还有剩余的障碍,且还能阻挡棋子,那么直接放置即可。
时间复杂度 \(\mathcal O(n\sqrt m)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
namespace F {
const int MAXV=2e5+5,MAXE=3e6+5,inf=1e9;
struct Edge {
int v,f,lst;
} G[MAXE];
int S,T,tot=1,siz,hd[MAXV],cur[MAXV],dep[MAXV];
void init() { tot=1,memset(hd,0,sizeof(int)*(siz+1)); }
void adde(int u,int v,int w) { G[++tot]={v,w,hd[u]},hd[u]=tot; }
int link(int u,int v,int w) { adde(u,v,w),adde(v,u,0); return tot-1; }
bool BFS() {
memcpy(cur,hd,sizeof(int)*(siz+1));
memset(dep,-1,sizeof(int)*(siz+1));
queue <int> Q;
Q.push(S),dep[S]=0;
while(!Q.empty()) {
int u=Q.front(); Q.pop();
for(int i=hd[u];i;i=G[i].lst) if(G[i].f&&dep[G[i].v]==-1) {
dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
}
}
return ~dep[T];
}
int dfs(int u,int f) {
if(u==T) return f;
int r=f;
for(int i=cur[u];i;i=G[i].lst) {
int v=G[cur[u]=i].v;
if(G[i].f&&dep[v]==dep[u]+1) {
int g=dfs(v,min(r,G[i].f));
if(!g) dep[v]=-1;
G[i].f-=g,G[i^1].f+=g,r-=g;
}
if(!r) return f;
}
return f-r;
}
int Dinic() {
int f=0;
while(BFS()) f+=dfs(S,inf);
return f;
}
}
const int MAXN=2e5+5;
int n,m,K,L[MAXN],R[MAXN],e[MAXN];
bool vis[MAXN],vl[MAXN],vr[MAXN];
struct poi { int x,y,i; } a[MAXN];
void solve() {
cin>>n>>m>>K,F::init(),K=m-K;
for(int i=1;i<=n;++i) cin>>a[i].x>>a[i].y,a[i].i=-1;
for(int i=1;i<=m;++i) cin>>a[i+n].x>>a[i+n].y,a[i+n].i=i;
for(int i=1;i<=m;++i) L[i]=R[i]=vis[i]=0;
int cl=0,cr=0,ot=0;
sort(a+1,a+n+m+1,[&](auto u,auto v){ return u.x^v.x?u.x<v.x:u.y<v.y; });
for(int i=1,j;i<=n+m;i=j) {
for(j=i;j<=n+m&&a[j].x==a[i].x;++j);
for(int k=i,p=0;k<j;++k) if(a[k].i==-1) {
if(p&&p!=k-1) {
++cl;
for(int o=p+1;o<=k-1;++o) L[a[o].i]=cl;
} else if(p) ++ot;
p=k;
}
}
sort(a+1,a+n+m+1,[&](auto u,auto v){ return u.y^v.y?u.y<v.y:u.x<v.x; });
for(int i=1,j;i<=n+m;i=j) {
for(j=i;j<=n+m&&a[j].y==a[i].y;++j);
for(int k=i,p=0;k<j;++k) if(a[k].i==-1) {
if(p&&p!=k-1) {
++cr;
for(int o=p+1;o<=k-1;++o) R[a[o].i]=cr;
} else if(p) ++ot;
p=k;
}
}
int s=F::S=cl+cr+1,t=F::T=F::siz=s+1;
for(int i=1;i<=m;++i) if(L[i]&&R[i]) e[i]=F::link(L[i],cl+R[i],1);
for(int i=1;i<=cl;++i) F::link(s,i,1),vl[i]=0;
for(int i=1;i<=cr;++i) F::link(cl+i,t,1),vr[i]=0;
int w=F::Dinic();
if(K<=w) {
cout<<cl+cr-2*K+ot<<"\n";
for(int i=1;i<=m&&K;++i) if(L[i]&&R[i]&&!F::G[e[i]].f) vis[i]=true,--K;
} else {
cout<<max(cl+cr-2*w-(K-w),0)+ot<<"\n";
for(int i=1;i<=m&&K;++i) if(L[i]&&R[i]&&!F::G[e[i]].f) vis[i]=vl[L[i]]=vr[R[i]]=true,--K;
for(int i=1;i<=m&&K;++i) if(!vis[i]) {
if(L[i]&&!vl[L[i]]) vis[i]=vl[L[i]]=true,--K;
if(R[i]&&!vr[R[i]]) vis[i]=vr[R[i]]=true,--K;
}
for(int i=1;i<=m&&K;++i) if(!vis[i]) vis[i]=true,--K;
}
for(int i=1;i<=m;++i) if(!vis[i]) cout<<i<<" "; cout<<"\n";
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
*D. Generated String
题目大意
动态维护字符串集合 \(A\),\(q\) 次操作支持插入删除,询问 \((s,t)\) 求出 \(A\) 中有多少字符串以 \(s\) 开头以 \(t\) 结尾。
所有的字符串均以模板串 \(S\) 的 \(k_i\) 个子串的拼接形式给出。
数据范围:\(\sum k\le 3\times 10^5,q\le 10^5\)。
思路分析
先考虑如果字符串是直接给出的怎么做,那么对所有串建 Trie,询问时一个串前缀合法就要求在 \(s\) 的子树内、
同理一个串后缀合法就要在反串 Trie 上在 \(t\) 的子树内。
加上每个串在 \(A\) 中的存在时间,可以把问题看成三维偏序,CDQ 分治解决,复杂度 \(\mathcal O(q\log^2q)\)。
因此我们只需要求出每个字符串在 Trie 树上的 dfn 序以及子树最大的 dfn 序。
先考虑 Trie 树上的 dfn 序怎么求,相当于把这些字符串按字典序排序,先二分 lcp, 比较哈希值,求哈希值再二分当前前缀落在哪个子串中,可以 \(\mathcal O(\log^2k)\) 求出 lcp。
然后考虑子树最大的 dfn 序,设字符串排序后得到 \(w_1\sim w_q\),那么 \(w_i\) 子树最大的 dfn 序等价于找到最大的 \(j\) 使得 \(w_i\) 是 \(w_j\) 前缀,可以直接二分。
时间复杂度 \(\mathcal O(q\log q\log^2k)\),瓶颈在排序,但常数相当小。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,MOD=1e9+21;
mt19937 rnd(time(0));
int bs,pw[MAXN],hc[MAXN],phs[MAXN];
char s[MAXN];
int qhs(int l,int r) { return (phs[r]+1ll*(MOD-pw[r-l+1])*phs[l-1])%MOD; }
int n,m;
struct String {
vector <int> L,R,hv;
vector <ll> siz;
ll len;
void read() {
int k; cin>>k;
L.resize(k),R.resize(k);
for(int i=0;i<k;++i) cin>>L[i]>>R[i];
}
void rev() {
swap(L,R);
for(int &i:L) i=2*n+1-i;
for(int &i:R) i=2*n+1-i;
reverse(L.begin(),L.end());
reverse(R.begin(),R.end());
}
void init() {
int k=L.size(); len=0;
hv.resize(k),siz.resize(k);
for(int i=0;i<k;++i) {
len+=R[i]-L[i]+1,siz[i]=len;
hv[i]=(1ll*(i?hv[i-1]:0)*pw[R[i]-L[i]+1]+qhs(L[i],R[i]))%MOD;
}
}
inline int qhv(ll x) const {
int i=lower_bound(siz.begin(),siz.end(),x)-siz.begin();
if(!i) return qhs(L[i],L[i]+x-1);
x-=siz[i-1];
return (1ll*hv[i-1]*pw[x]+qhs(L[i],L[i]+x-1))%MOD;
}
inline char qc(ll x) const {
int i=lower_bound(siz.begin(),siz.end(),x)-siz.begin();
x-=i?siz[i-1]:0;
return s[L[i]+x-1];
}
};
ll lcp(const String &u,const String &v) {
ll l=1,r=min(u.len,v.len),x=0;
while(l<=r) {
ll mid=(l+r)>>1;
if(u.qhv(mid)==v.qhv(mid)) x=mid,l=mid+1;
else r=mid-1;
}
return x;
}
char op[MAXN];
int del[MAXN],id[MAXN];
void build(String *S,int *st,int *ed) {
int q=0;
for(int i=1;i<=m;++i) if(op[i]!='-') id[++q]=i;
sort(id+1,id+q+1,[&](int x,int y){
ll k=lcp(S[x],S[y]);
if(k==min(S[x].len,S[y].len)) {
if(S[x].len^S[y].len) return S[x].len<S[y].len;
return op[x]=='?'&&op[y]=='+';
}
return S[x].qc(k+1)<S[y].qc(k+1);
});
for(int i=1;i<=q;++i) {
st[id[i]]=i;
const String &str=S[id[i]];
int l=i+1,r=q,k=i,e=str.len,t=str.hv.back();
while(l<=r) {
int mid=(l+r)>>1;
auto chk=[&](const String &v) {
return v.len>=e&&v.qhv(e)==t;
};
if(chk(S[id[mid]])) k=mid,l=mid+1;
else r=mid-1;
}
ed[id[i]]=k;
}
}
String A[MAXN],B[MAXN];
int st1[MAXN],ed1[MAXN],st2[MAXN],ed2[MAXN];
struct opr { int x,l,r,id,k; } a[MAXN*2];
int ans[MAXN];
struct FenwickTree {
int tr[MAXN],st[MAXN],tp,s;
void add(int x,int v) { for(st[++tp]=x;x<=m;x+=x&-x) tr[x]+=v; }
int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
void init() { while(tp) for(int x=st[tp--];x<=m;x+=x&-x) tr[x]=0; }
} T;
void cdq(int l,int r) {
if(l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid),cdq(mid+1,r);
T.init();
for(int i=mid+1,j=l;i<=r;++i) {
for(;j<=mid&&a[j].x<=a[i].x;++j) if(!a[j].id) T.add(a[j].l,a[j].k);
if(a[i].id) ans[a[i].id]+=a[i].k*(T.qry(a[i].r)-T.qry(a[i].l-1));
}
inplace_merge(a+l,a+mid+1,a+r+1,[&](auto i,auto j){ return i.x<j.x; });
}
signed main() {
bs=rnd()%MOD,pw[0]=1;
for(int i=1;i<MAXN;++i) pw[i]=1ll*pw[i-1]*bs%MOD;
for(int c=0;c<26;++c) hc[c]=rnd()%MOD;
ios::sync_with_stdio(false);
cin>>n>>m>>(s+1);
for(int i=1;i<=n;++i) s[2*n+1-i]=s[i];
for(int i=1;i<=2*n;++i) phs[i]=(1ll*phs[i-1]*bs+hc[s[i]-'a'])%MOD;
for(int i=1;i<=m;++i) {
cin>>op[i];
if(op[i]=='-') cin>>del[i];
else if(op[i]=='?') {
A[i].read(),B[i].read(),B[i].rev();
A[i].init(),B[i].init();
} else {
A[i].read(),B[i]=A[i],B[i].rev();
A[i].init(),B[i].init();
}
}
build(A,st1,ed1),build(B,st2,ed2);
int q=0;
for(int i=1;i<=m;++i) {
if(op[i]=='?') {
a[++q]={st1[i]-1,st2[i],ed2[i],i,-1};
a[++q]={ed1[i],st2[i],ed2[i],i,1};
} else {
int x=(op[i]=='-')?del[i]:i;
a[++q]={st1[x],st2[x],0,0,op[i]=='-'?-1:1};
}
}
cdq(1,q);
for(int i=1;i<=m;++i) if(op[i]=='?') cout<<ans[i]<<"\n";
return 0;
}
Round #31 - 2024.11.14
H. Huge Segment Tree
题目大意
给定 \(2^n\) 个节点的标准线段树,对每个 \(i\) 求出有多少个 \([l,r]\) 恰好在线段树上拆成 \(i\) 个节点。
数据范围:\(n\le 5\times 10^5\)。
思路分析
考虑 zkw 线段树的过程,取 \(l-1\) 和 \(r+1\),然后找到从高到低找到第一个 \(l-1\) 与 \(r+1\) 不同的二进制位 \(k\),那么拆出的节点个数就是 \(0\sim k-1\) 位中 \(l-1\) 的 \(0\) 的个数加上 \(r+1\) 的 \(1\) 的个数。
然后特判 \(l=0\) 和 \(r=2^n\) 的情况,我们就是要计算 \(f_i=\sum\limits_{k=1}^{n-1}2^{n-k-1}\binom{2k}i\),写成差卷积的形式用 NTT 快速计算即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
namespace P {
const int MOD=998244353,N=1<<21,G=3;
int rev[N],inv[N],fac[N],ifac[N],w[N<<1];
int ksm(int a,int b=MOD-2) {
int ret=1;
for(;b;a=1ll*a*a%MOD,b=b>>1) if(b&1) ret=1ll*ret*a%MOD;
return ret;
}
void poly_init() {
inv[1]=1;
for(int i=2;i<N;++i) inv[i]=1ll*(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[0]=ifac[0]=1;
for(int i=1;i<N;++i) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*inv[i]%MOD;
for(int k=1;k<=N;k<<=1) {
int x=ksm(G,(MOD-1)/k); w[k]=1;
for(int i=1;i<k;++i) w[i+k]=1ll*x*w[i+k-1]%MOD;
}
}
int plen(int x) { int y=1; for(;y<x;y<<=1); return y; }
void ntt(int *f,bool idft,int n) {
for(int i=0;i<n;++i) {
rev[i]=(rev[i>>1]>>1);
if(i&1) rev[i]|=n>>1;
}
for(int i=0;i<n;++i) if(rev[i]<i) swap(f[i],f[rev[i]]);
for(int k=2,x,y;k<=n;k<<=1) {
for(int i=0;i<n;i+=k) {
for(int j=i;j<i+k/2;++j) {
x=f[j],y=1ll*f[j+k/2]*w[k+j-i]%MOD;
f[j]=(x+y>=MOD)?x+y-MOD:x+y,f[j+k/2]=(x>=y)?x-y:x+MOD-y;
}
}
}
if(idft) {
reverse(f+1,f+n);
for(int i=0,x=ksm(n);i<n;++i) f[i]=1ll*f[i]*x%MOD;
}
}
void poly_mul(const int *f,const int *g,int *h,int n,int m) {
static int a[N],b[N];
for(int i=0;i<n;++i) a[i]=f[i];
for(int i=0;i<m;++i) b[i]=g[i];
int len=plen(n+m-1);
ntt(a,0,len),ntt(b,0,len);
for(int i=0;i<len;++i) h[i]=1ll*a[i]*b[i]%MOD;
ntt(h,1,len);
memset(a,0,sizeof(int)*len);
memset(b,0,sizeof(int)*len);
}
}
const int MAXN=1e6+5,N=1<<21,MOD=998244353;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
ll f[MAXN],fac[MAXN],ifac[MAXN],pw[MAXN];
ll C(int x,int y) {
if(x<0||y<0||y>x) return 0;
return fac[x]*ifac[y]%MOD*ifac[x-y]%MOD;
}
int a[N],b[N],c[N];
signed main() {
P::poly_init();
for(int i=fac[0]=pw[0]=1;i<MAXN;++i) pw[i]=pw[i-1]*2%MOD,fac[i]=fac[i-1]*i%MOD;
ifac[MAXN-1]=ksm(fac[MAXN-1]);
for(int i=MAXN-1;i;--i) ifac[i-1]=ifac[i]*i%MOD;
ios::sync_with_stdio(false);
int n; cin>>n;
for(int i=1;i<n;++i) a[2*(n-i)]=fac[2*(n-i)]*pw[i-1]%MOD;
for(int i=0;i<=2*n-2;++i) b[2*n-i]=ifac[i];
P::poly_mul(a,b,c,2*n,2*n+1);
for(int i=1;i<=2*n-2;++i) f[i]=ifac[i]*c[i+2*n]%MOD;
for(int i=1;i<=n;++i) f[i]=(f[i]+2*C(n,i))%MOD;
f[1]=(f[1]+1)%MOD;
for(int i=1;i<=2*n-2;++i) cout<<f[i]<<" "; cout<<"\n";
return 0;
}
B. Black or White 2
题目大意
给定 \(n\times m\) 网格,将其中 \(k\) 个格子染黑,最小化恰有两个黑格的 \(2\times 2\) 子矩阵个数,给出构造。
数据范围:\(n,m\le 1500\)。
思路分析
手玩一下发现,一个比较优秀的构造是取一个矩形,然后在四条边界上间隔染色,中间全部染色,此时任何一个 \(2\times 2\) 的黑格数都 \(\ne 2\)。
能么一种可能的构造方式如下:
- 给第 \(1\) 行的 \(1,3,5,7,\dots\) 列染黑。
- 同时给第 \(1,2\) 行的 \(2,4,6,8,\dots\) 列染黑。
- 同时给第 \(2,3\) 行的 \(1,3,5,7,\dots\) 列染黑。
- 同时给第 \(3,4\) 行的 \(2,4,6,8,\dots\) 列染黑。
- 同时给第 \(4,5\) 行的 \(1,3,5,7,\dots\) 列染黑。
- \(\dots\dots\)
不断重复这个过程,最终会剩余 \(<2\) 个格子,如果有的话染黑 \((n,m)\) 即可。
为了保证能构造,需要调整使得 \(n\ge m\) 且 \(k\le \dfrac 12nm\)。
时间复杂度 \(\mathcal O(nm)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1505;
bool a[MAXN][MAXN];
void solve() {
int n,m,k;
cin>>n>>m>>k;
bool sw=0,rv=0;
if(n<m) swap(n,m),sw=1;
if(k*2>n*m) k=n*m-k,rv=1;
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) a[i][j]=0;
for(int j=1;j<=m&&k;j+=2) a[1][j]=1,--k;
if(k&1) a[n][m]=1,--k;
for(int i=2;i<=n&&k;++i) for(int j=2-(i&1);j<=m&&k;j+=2) a[i][j]=a[i-1][j]=1,k-=2;
if(sw) swap(n,m);
for(int i=1;i<=n;++i,cout<<"\n") for(int j=1;j<=m;++j) cout<<((sw?a[j][i]:a[i][j])^rv);
}
signed main() {
ios::sync_with_stdio(false);
int T; cin>>T;
while(T--) solve();
return 0;
}
K. Kth Sum
题目大意
给定 \(a_1\sim a_n,b_1\sim b_n,c_1\sim c_n\),求 \(a_x+b_y+c_z\) 的第 \(k\) 小。
数据范围:\(n\le 5\times 10^4,k\le 10^9\)。
思路分析
先对 \(a,b,c\) 排序,二分后转成数 \(a_x+b_y+c_z\le v\) 的元素是否 \(\ge k\) 个。
首先如果枚举 \(x\),数 \((y,z)\) 的过程是双指针,需要 \(\mathcal O(n)\)。
但我们发现枚举前 \(B\) 小的 \(a_x\) 之后,此后的某个 \(a_x\) 如果满足条件的 \((y,z)\) 数量 \(>\dfrac{k}B\),那么前 \(B\) 小的 \(a_x\) 对应的 \((y,z)\) 数量全部 \(>\dfrac kB\),从而 \((x,y,z)\) 对数 \(>k\)。
因此我们可以把 \(>B\) 小的 \(a_x\) 数的 \((y,z)\) 对数和 \(\dfrac kB\) 取 \(\min\),也即只关心前 \(\dfrac kB\) 小的 \(b_y+c_z\),这部分可以优先队列预处理,查询的时候将 \((y,z)\) 排序后依然双指针。
单次查询复杂度 \(\mathcal O\left(nB+\dfrac kB\right)\),取 \(B=\sqrt{\dfrac kn}\) 得到最优复杂度 \(\mathcal O(\sqrt {nk})\)。
时间复杂度 \(\mathcal O(\sqrt {nk}\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e4+5,MAXS=8e6+5;
ll a[MAXN],b[MAXN],c[MAXN],f[MAXS];
int n,K,B,S;
bool chk(ll v) {
int cnt=0;
for(int i=1;i<=n&&i<=B;++i) {
for(int j=1,k=n;j<=n;++j) {
while(k&&a[i]+b[j]+c[k]>v) --k;
if((cnt+=k)>=K) return false;
}
}
for(int i=B+1,j=S;i<=n;++i) {
while(j&&a[i]+f[j]>v) --j;
if((cnt+=j)>=K) return false;
}
return true;
}
signed main() {
ios::sync_with_stdio(false);
cin>>n>>K,B=sqrt(K/n)+1,S=min(K/B+1ll,1ll*n*n);
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=1;i<=n;++i) cin>>b[i];
for(int i=1;i<=n;++i) cin>>c[i];
sort(a+1,a+n+1),sort(b+1,b+n+1),sort(c+1,c+n+1);
priority_queue <array<ll,2>,vector<array<ll,2>>,greater<array<ll,2>>> Q;
for(int i=1;i<=n;++i) Q.push({b[i]+c[1],1});
for(int i=1;i<=S;++i) {
auto it=Q.top(); Q.pop(),f[i]=it[0];
if(it[1]<n) Q.push({it[0]+c[it[1]+1]-c[it[1]],it[1]+1});
}
ll l=a[1]+b[1]+c[1],r=a[n]+b[n]+c[n],x=l-1;
while(l<=r) {
ll mid=(l+r)>>1;
if(chk(mid)) x=mid,l=mid+1;
else r=mid-1;
}
cout<<x+1<<"\n";
return 0;
}
O. Optimal Train Operation
题目大意
给定 \(0\sim n\) 数轴,初始 \(0,n\) 上有点,可以花 \(a_i\) 的代价在 \(i\) 上放点,也可以选定两个点 \(x,y\),花费 \(y-x\) 的代价覆盖 \([x,y]\) 一次,要求 \([i-1,i]\) 覆盖 \(\ge c_i\) 次,求最小代价。
数据范围:\(n\le 5\times 10^5\)。
思路分析
观察题目,我们发现如果有三个点 \(i,j,k\),那么覆盖 \([i,k]\) 等价覆盖 \([i,j]+[j,k]\),因此最优解一定都是覆盖相邻两个点。
考虑 dp,设 \(f_i\) 表示在 \(i\) 上放点时,满足 \([0,i]\) 条件的最小代价,转移为 \(f_i=a_i+\min_{j<i}\{(i-j)\max_{j<k\le i}c_k+f_j\}\)。
这种包含区间最大值的形式的问题,可以考虑 CDQ 分治转移,维护 \([l,mid]\) 的后缀最大值和 \([mid+1,r]\) 的前缀最大值。
得到 \(f_i=a_i+\min \{(i-j)\max(mx_i,mx_j)+f_j\}\),分讨 \(mx_i,mx_j\) 的关系:
- 如果 \(mx_i<mx_j\),那么转移为 \(a_i+\min\{ i\times mx_j-j\times mx_j+f_j\}\),倒序枚举 \(i\),用李超线段树维护。
- 否则转移为 \(a_i+i\times mx_i+\min \{-j\times mx_i+f_j\}\),顺序枚举 \(i\),用李超线段树维护。
建立动态开点李超线段树即可。
时间复杂度 \(\mathcal O(n\log n\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5,V=1e9;
const ll inf=1e18;
struct func {
ll k,b;
inline ll f(const ll &x) const { return x*k+b; }
} F[MAXN];
struct SegmentTree {
int tr[MAXN],ls[MAXN],rs[MAXN],tot;
void init() {
for(int i=1;i<=tot;++i) tr[i]=ls[i]=rs[i]=0;
tot=0;
}
void ins(int i,int l,int r,int &p) {
if(!p) return tr[p=++tot]=i,void();
int mid=(l+r)>>1;
if(F[tr[p]].f(mid)>F[i].f(mid)) swap(tr[p],i);
if(l==r) return ;
if(F[tr[p]].f(l)>F[i].f(l)) ins(i,l,mid,ls[p]);
if(F[tr[p]].f(r)>F[i].f(r)) ins(i,mid+1,r,rs[p]);
}
ll qry(int x,int l,int r,int p) {
if(!p) return inf;
ll w=F[tr[p]].f(x);
if(l==r) return w;
int mid=(l+r)>>1;
if(x<=mid) w=min(w,qry(x,l,mid,ls[p]));
else w=min(w,qry(x,mid+1,r,rs[p]));
return w;
}
} T;
int n,rt;
ll a[MAXN],b[MAXN],dp[MAXN],mx[MAXN];
void cdq(int l,int r) {
if(l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid);
mx[mid]=0,mx[mid+1]=b[mid+1];
for(int i=mid+2;i<=r;++i) mx[i]=max(mx[i-1],b[i]);
for(int i=mid;i>l;--i) mx[i-1]=max(mx[i],b[i]);
T.init(),rt=0;
for(int i=mid+1,j=mid;i<=r;++i) {
for(;j>=l&&mx[j]<=mx[i];--j) F[j]={-j,dp[j]},T.ins(j,0,V,rt);
dp[i]=min(dp[i],T.qry(mx[i],0,V,rt)+i*mx[i]+a[i]);
}
T.init(),rt=0;
for(int i=r,j=l;i>mid;--i) {
for(;j<=mid&&mx[j]>mx[i];++j) F[j]={mx[j],dp[j]-j*mx[j]},T.ins(j,0,V,rt);
dp[i]=min(dp[i],T.qry(i,0,V,rt)+a[i]);
}
cdq(mid+1,r);
}
signed main() {
cin>>n;
F[0]={0,inf};
for(int i=1;i<=n;++i) cin>>b[i];
for(int i=1;i<n;++i) cin>>a[i];
memset(dp,0x3f,sizeof(dp)),dp[0]=0;
cdq(0,n);
cout<<dp[n]<<"\n";
return 0;
}
G. Graph Weighting
题目大意
给定一个 \(n\) 个点 \(m\) 条边的无向图,给每个点赋 \([0,L]\) 权值,对于 \(s=0\sim K\) 求出使得每个生成树边权和 \(=s\) 时,边权平方和的最小值。
数据范围:\(n,L,K\le 10^5,m\le 2\times 10^5\)。
思路分析
先考虑每个生成树边权和 \(=s\) 时什么条件,任取一棵生成树,对于其他非树边,对应的树上路径边权都要和这条边相等。
那么可以推出充要条件是图中每个点双联通分量边权相等。
因此对于一个点双联通分量,我们只关心 \((a_i,b_i,x_i)\) 表示点数 \(-1\),边数和权值,那么对应体积为 \(a_ix_i\),权值为 \(b_ix_i^2\)。
由于体积是 \(a_i\) 的倍数不好转移,但 \(\sum a=\mathcal O(n)\),因此可以对相同的 \(a_i\) 合并,得到 \(\mathcal O(\sqrt n)\) 种本质不同的 \(a_i\)。
对于每种 \(a_i\),我们只需要对每个 \(k\) 算出体积 \(ka_i\) 时最小的权值和 \(w_k\),可以对 \(x\) 拆权值,即 \(x_i\) 从 \(x\to x+1\) 时产生体积为 \(a_i\) 权值为 \(b_i(2x+1)\) 的物品。
由于使用的物品不超过 \(\dfrac{n}{a_i}\) 个,因此用堆维护可能的最小值即可。
把背包下标按 \(\bmod a_i\) 的余数分类,转成若干体积 \(=1\) 的物品,相当于 \(f'_i=\min(f_i+w_k)\) 的转移。
显然 \(w_k\) 是凸函数,故满足四边形不等式,因此转移具有决策单调性,可以分治优化。
时间复杂度 \(\mathcal O(n\log^2 n+K\sqrt n\log K)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
const ll inf=1e18;
int n,m,K,U;
vector <int> G[MAXN];
int dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp,scnt,bel[MAXN];
bool ins[MAXN];
int vc[MAXN],ec[MAXN];
void tarjan(int u) {
dfn[u]=low[u]=++dcnt,stk[++tp]=u,ins[u]=true;
for(int v:G[u]) {
if(!dfn[v]) {
tarjan(v),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) {
++scnt,++vc[scnt];
while(ins[v]) bel[stk[tp]]=scnt,++vc[scnt],ins[stk[tp--]]=false;
}
} else if(ins[v]) low[u]=min(low[u],dfn[v]);
}
}
int S;
vector <int> W[MAXN];
ll dp[MAXN],f[MAXN],g[MAXN],c[MAXN];
void solve(int l,int r,int L,int R) {
if(l>r) return ;
int mid=(l+r)>>1,p=0;
for(int i=max(L,mid-S);i<=R&&i<=mid;++i) {
ll v=f[i]+c[mid-i];
if(v<=g[mid]) g[mid]=v,p=i;
}
solve(l,mid-1,L,p),solve(mid+1,r,p,R);
}
signed main() {
ios::sync_with_stdio(false);
cin>>n>>m>>K>>U;
for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
tarjan(1);
for(int u=1;u<=n;++u) for(int v:G[u]) if(dfn[v]<dfn[u]) ++ec[bel[u]];
for(int i=1;i<=scnt;++i) W[vc[i]-1].push_back(ec[i]);
for(int i=1;i<=K;++i) dp[i]=inf;
for(int i=1;i<=n;++i) if(W[i].size()) {
priority_queue <array<ll,3>,vector<array<ll,3>>,greater<array<ll,3>>> Q;
for(int w:W[i]) Q.push({w,1,w});
c[0]=0;
for(S=0;Q.size()&&S<K/i;++S) {
auto w=Q.top(); Q.pop();
c[S+1]=c[S]+w[0];
if(w[1]<U) Q.push({w[0]+2*w[2],w[1]+1,w[2]});
}
for(int r=0;r<i;++r) {
int up=(K-r)/i;
for(int j=0;j<=up;++j) f[j]=dp[r+i*j],g[j]=inf;
solve(0,up,0,up);
for(int j=0;j<=up;++j) dp[r+i*j]=g[j];
}
}
for(int i=0;i<=K;++i) cout<<(dp[i]>=inf?-1:dp[i])<<" \n"[i==K];
return 0;
}
*M. Majority and Permutation
题目大意
给定一个奇数构成的集合 \(A\),定义一个长度为 \(2n\) 的 01 串 \(s\) 是好的,当且仅当 \(0,1\) 各出现 \(n\) 次,且 \(\forall k\in A\),\(s[1,k]\) 的众数都是 \(0\)。
求有多少长度为 \(2n\) 的排列 \(p\) 使得存在好的 01 串 \(s\),使得 \(t=s_{p_1}s_{p_2}\dots s_{p_{2n}}\) 是好的。
数据范围:\(n\le 10^5\)。
思路分析
考虑已知 \(p\) 如何构造 \(s\),尝试贪心地构造,那么我们对于每个 \(k\in A\),在保证 \(s[1,k]\) 中有 \(\dfrac{k+1}2\) 个 \(0\) 的基础上,最大化 \(t\) 中前缀 \(0\) 的个数。
这可以用堆维护,即用堆维护 \(p_1\sim p_k\) 中没填 \(0\) 的位置,如果 \(0\) 个数 \(<\dfrac{k+1}2\),那么找最小的 \(p_i\) 填 \(0\),这样可以让 \(t\) 中的 \(0\) 尽可能靠前。
分析什么时候这样构造出来的字符串不合法,那么一定是 \(t\) 中某个前缀 \(t[1,k]\) 不合法,那么相当于 \(t[k+1,2n]\) 中绝对众数也是 \(0\)。
可以证明这种情况只可能是 \(2n-k\in A\) 且 \(p_1\sim p_{2n-k}\) 恰好是 \(k+1\sim 2n\)。
那么我们只保留 \(2n-k\in A\) 的元素在 \(A\) 中,就要求每个 \(p[1,k]\) 不是 \(2n-k+1\sim 2n\)。
可以容斥,钦定若干 \(A\) 中元素 \(k\) 满足 \(p[1,k]\) 是 \(2n-k+1\sim 2n\)。
设被钦定的元素是 $a_1,a_2,a_3\dots $,那么相当于对于每个 \(p(a_{i-1},a_i]\) 的值域都被确定,总的方案数就是 \(\prod (a_i-a_{i-1})!\)。
因此设 \(f_i\) 表示钦定容斥 \(i\) 后的方案,那么转移就是 \(f_i=-\sum_{j\in A}(i-j)!f_j\),分治 NTT 加速计算即可。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
namespace P {
const int MOD=998244353,N=1<<19,G=3;
int rev[N],inv[N],fac[N],ifac[N],w[N<<1];
int ksm(int a,int b=MOD-2) {
int ret=1;
for(;b;a=1ll*a*a%MOD,b=b>>1) if(b&1) ret=1ll*ret*a%MOD;
return ret;
}
void poly_init() {
inv[1]=1;
for(int i=2;i<N;++i) inv[i]=1ll*(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[0]=ifac[0]=1;
for(int i=1;i<N;++i) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*inv[i]%MOD;
for(int k=1;k<=N;k<<=1) {
int x=ksm(G,(MOD-1)/k); w[k]=1;
for(int i=1;i<k;++i) w[i+k]=1ll*x*w[i+k-1]%MOD;
}
}
int plen(int x) { int y=1; for(;y<x;y<<=1); return y; }
void ntt(int *f,bool idft,int n) {
for(int i=0;i<n;++i) {
rev[i]=(rev[i>>1]>>1);
if(i&1) rev[i]|=n>>1;
}
for(int i=0;i<n;++i) if(rev[i]<i) swap(f[i],f[rev[i]]);
for(int k=2,x,y;k<=n;k<<=1) {
for(int i=0;i<n;i+=k) {
for(int j=i;j<i+k/2;++j) {
x=f[j],y=1ll*f[j+k/2]*w[k+j-i]%MOD;
f[j]=(x+y>=MOD)?x+y-MOD:x+y,f[j+k/2]=(x>=y)?x-y:x+MOD-y;
}
}
}
if(idft) {
reverse(f+1,f+n);
for(int i=0,x=ksm(n);i<n;++i) f[i]=1ll*f[i]*x%MOD;
}
}
void poly_mul(const int *f,const int *g,int *h,int n,int m) {
static int a[N],b[N];
for(int i=0;i<n;++i) a[i]=f[i];
for(int i=0;i<m;++i) b[i]=g[i];
int len=plen(n+m-1);
ntt(a,0,len),ntt(b,0,len);
for(int i=0;i<len;++i) h[i]=1ll*a[i]*b[i]%MOD;
ntt(h,1,len);
memset(a,0,sizeof(int)*len);
memset(b,0,sizeof(int)*len);
}
}
const int N=1<<19,MOD=998244353;
int n,m,dp[N],A[N],B[N],C[N];
bool e[N];
void cdq(int l,int r) {
if(l==r) return ;
int mid=(l+r)>>1;
cdq(l,mid);
for(int i=l;i<=mid;++i) A[i-l]=dp[i];
for(int i=0;i<=r-l;++i) B[i]=P::fac[i];
P::poly_mul(A,B,C,mid-l+1,r-l+1);
for(int i=mid+1;i<=r;++i) if(e[i]) dp[i]=(dp[i]+MOD-C[i-l])%MOD;
cdq(mid+1,r);
}
signed main() {
P::poly_init();
scanf("%d%d",&n,&m),n*=2;
for(int i=1,x;i<=m;++i) scanf("%d",&x),e[x]=true;
for(int i=1;i<=n;++i) e[i]&=e[n-i];
e[n]=true,dp[0]=1,cdq(0,n);
printf("%d\n",(MOD-dp[n])%MOD);
return 0;
}
*P. Priority Queue 3
题目大意
给定长度为 \(n+m\) 的操作序列,\(n\) 个 push 操作和 \(m\) 个 pop 操作。
有一个小根堆,每次 push 操作可以选一个 \(1\sim n\) 中没被 push 的元素插入堆,pop 操作把最小值弹出并插入 \(A\)。
给定最终的 \(A_0\),求有多少种合法的 push 方法。
数据范围:\(n,m\le 300\)。
思路分析
设 \(A_0\) 中的元素升序排列得到 \(a_1\sim a_m\)。
先刻画一组合法的 push 序列,如果我们 push 了一个 \(\not\in A_0\) 元素,那么这个元素应该大于所有 \(A_0-A\) 中的元素,才能使得 \(A_0-A\) 的元素在后续 pop 的时候不会被这个元素卡住。
因此我们只关心 \(\max(A_0-A)\),在 dp 的时候记录 \(f_{i,j}\) 表示前 \(i\) 个操作得到 \(\max(A_0-A)=a_j\)。
但是遇到 pop 操作的时候,我们并不知道 \(a_j\) 是否被弹出,因此需要记录当前堆中有几个 \(A_0\) 的元素,以及 \(a_j\) 是否在堆中。
那么状态变为 \(f_{i,j,k,o}\),其中 \(k\) 表示堆中 \(A_0\) 元素数量,\(o\) 表示 \(a_j\) 是否在堆中。
另一个问题是如果操作的元素 \(\in A_0\) 且 \(\ne a_j\),那么我们难以记录这些元素具体是什么。
考虑这些元素什么时候会产生贡献,当且仅当 pop 掉 \(a_j\) 后,如果 \(a_{j-1}\) 在这之前被 pop,那么新的 \(j\) 会更小。
可以在转移的时候枚举新的 \(j'\),然后处理 \(a_{j'+1}\sim a_{j-1}\) 在什么时候被 pop 掉,相当于 \(f_{i,j,k,o}\) 只考虑 \(a_{j}\sim a_m\) 中的元素。
那么为了处理 \(a_{j'+1}\sim a_{j-1}\) 的插入时间,我们在 dp 到 push 操作的时候的时候插入且不选择具体 \(a\),当 pop \(a_j\) 时把他们对应到 \(1\sim i-1\) 中的某些 push 操作上。
由于 pop 掉 \(a_j\) 时一定 pop 了所有其他 \(\in A_0\) 的元素,因此此前预留的每个 push 都合法,用组合数选定位置即可。
讨论转移,设当前这在处理第 \(1\sim i\) 次操作操作,且 \(1\sim i-1\) 中有:
-
如果 push 了一个 \(\not\in A_0\) 元素,可能的元素个数为 \((n-a_j)-(m-j)-(x-y-k)\),转移 \(f_{i-1,j,k,o}\to f_{i,j,k,o}\)。
-
如果 push 了 \(a_j\),\(f_{i-1,j,k,0}\to f_{i,j,k+1,1}\)。
-
如果 push 了其他 \(A_0\) 中元素,暂不考虑该元素的值,\(f_{i-1,j,k,o}\to f_{i,j,k+1,o}\)。
-
如果 pop 了 \(\ne a_j\) 的元素,\(f_{i-1,j,k,o}\to f_{i,j,k-1,o}\)。
-
如果 pop 了 \(a_j\),要求 \(k=o=1\),枚举 \(j'<j\) 有 \(f_{i-1,j,1,1}\to f_{i,j',0,0}\)。
此时预留的 push 操作数量等于预留的 pop 操作数量,即 \(y-(m-j)\),故方案数为 \((y-(m-j))^{\underline{j-j'-1}}\)。
按照上述过程 dp。
时间复杂度 \(\mathcal O(nm^2)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=305,MOD=998244353;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
inline void add(ll &x,const ll &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int n,m,a[MAXN];
char str[MAXN*2];
ll fac[MAXN],ifac[MAXN];
ll f[MAXN][MAXN][2],g[MAXN][MAXN][2];
ll A(int x,int y) { return fac[x]*ifac[x-y]%MOD; }
//max not pop = j, now inq = k, [j inq?]
signed main() {
scanf("%d%d%s",&n,&m,str+1);
for(int i=1;i<=m;++i) scanf("%d",&a[i]);
for(int i=fac[0]=ifac[0]=1;i<=n;++i) ifac[i]=ksm(fac[i]=fac[i-1]*i%MOD);
f[m][0][0]=1;
for(int i=1,ci=0,ce=0;i<=n+m;++i) {
memset(g,0,sizeof(g));
if(str[i]=='+') {
for(int j=0;j<=m;++j) for(int k=0;k<=min(i,j);++k) {
for(int o:{0,1}) {
int cnt=(n-a[j])-(m-j)-(ci-ce-k);
if(cnt>0&&ci-ce-k>=0) add(g[j][k][o],f[j][k][o]*cnt%MOD);
add(g[j][k+1][o],f[j][k][o]);
}
add(g[j][k+1][1],f[j][k][0]);
}
++ci;
} else {
for(int j=0;j<=m;++j) {
for(int k=1;k<=min(i,j);++k) for(int o:{0,1}) if(k>1||!o) add(g[j][k-1][o],f[j][k][o]);
int cnt=ce-(m-j);
for(int nj=j-1;~nj;--nj) if(j-nj-1<=cnt) {
add(g[nj][0][0],f[j][1][1]*A(cnt,j-nj-1)%MOD);
}
}
++ce;
}
memcpy(f,g,sizeof(f));
}
printf("%lld\n",f[0][0][0]);
return 0;
}
*F. Flip or Not
题目大意
给定长度为 \(n\) 的 01 串 \(s\),你可以进行 \(\le10^6\) 次操作,每次操作依次进行如下步骤:
- 将 \(s_0,\dots,s_{n-1}\) 变成 \(t=s_{n-1},s_{0},\dots ,s_{n-2}\),如果 \(s_{n-1}=1\),则翻转 \(t_{a_1}\sim t_{a_p}\)。
- 可以选择是否同时翻转 \(t_{b_1}\sim t_{b_q}\),然后 \(s\gets t\)。
给定 \(s,t\) 构造一组操作的方案使得该串从 \(s\) 变成 \(t\)。
数据范围:\(n\le 5000\)。
思路分析
用 \(\bmod 2\) 意义下的多项式操作刻画上述过程,记 \(S=\sum_{i=0}^{n-1}x^{s_i},T=\sum_{i=0}^{n-1} x^{t_i}\)。
定义 \(B=\sum_{i=1}^q x^{b_i}\),那么翻转 \(t_{b_1}\sim t_{b_q}\) 等价于 \(S\gets S+B\)。
第一步操作比较麻烦,首先循环移位等价于 \(S\gets xS\),如果 \([x^n]xS=1\),那么我们会翻转 \(a_1\sim a_p\) 位,加上 \(S_n\to S_0\),相当于 \(A=x^0+x^n+\sum_{i=1}^p x^{a_i}\),然后 \(S\gets xS\bmod A\)。
因此假设共操作 \(k\) 次,且第 \(i\) 次操作当且仅当 \(u_i=1\) 时翻转 \(b_1\sim b_q\),那么最终的 \(s\) 就是 \(x^kS+\sum_{i=1}^k u_ix^{k-i}B\bmod A\)。
设 \(U=\sum _{i=1}^k u_ix^{k-i}\),那么要求 \(T=x^kS+UB\bmod A\),其中 \(U\) 可以是任意 \(\le k-1\) 次多项式。
那么假设 \(x^kS+UB\bmod A=VA+T\),我们就是要解一个形如 \(PA+QB=x^kS+T\),其中 \(\mathrm{deg}(Q)<k\)。
注意到这是一个类似不定方程的问题,可以套用正整数域上的 exgcd,即用多项式除法代替正整数域上的带余除法,类似定义因数倍数关系。
那么推广裴蜀定理得到存在 \(P_0,Q_0\) 使得 \(P_0A+Q_0B=x^kS+T\) 当且仅当 \(x^kS+T\bmod \gcd(A,B)=0\)。
得到一组特解 \(P_0,Q_0\),通解可以被表示成 \(P_0+K\dfrac BG,Q_0-K\dfrac AG\) 的形式,我们要让 \(Q\) 的次数最小,肯定取 \(Q_0\bmod \dfrac{A_0}G\) 时最优。
设 \(G=\gcd(A,B)\),先做类似 exgcd 的过程求 \(P_gA+Q_gB=G\),然后枚举 \(k\),如果 \(x^kS+T\bmod G=0\),那么检验 \(Q_g\dfrac{x^kS+T}{G}\bmod \dfrac AG\) 的度数是否 \(\le k\)。
这个式子并不好维护,但很显然能变形成 \(\dfrac{Q_g(x^kS+T)\bmod A}G\),只需要动态维护 \(Q_g(x^kS+T)\),复杂度可以接受。
用 std::bitset 优化上述维护多项式加减乘除模的过程。
注意到计算多项式除法 \(A/B\) 的复杂度是 \(\mathcal O\left(\dfrac{n(\deg A-\deg B)}\omega\right)\),因此 exgcd 的过程总复杂度 \(\mathcal O\left(\dfrac{n^2}\omega\right)\)。
时间复杂度 \(\mathcal O\left(\dfrac{nm}\omega\right)\),其中 \(m=10^6\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int N=1e4;
typedef bitset<N+5> poly;
inline int deg(const poly &u) {
for(int i=N;~i;--i) if(u[i]) return i;
return 0;
}
inline poly operator *(const poly &x,const poly &y) {
static poly z,o; z.reset();
for(int i=0;i<N;++i) if(y[i]) o=x,o<<=i,z^=o;
return z;
}
inline array<poly,2> divp(poly x,const poly &y) {
static poly z,o; z.reset();
int dy=deg(y);
for(int i=N-dy;~i;--i) if(x[i+dy]) z.set(i),o=y,o<<=i,x^=o;
return {z,x};
}
inline poly operator /(const poly &x,const poly &y) {
return divp(x,y)[0];
}
inline poly operator %(const poly &x,const poly &y) {
return divp(x,y)[1];
}
inline array<poly,2> exgcd(poly x,poly y) { //px+qy=g
poly p,q; p.set(0);
while(y.any()) {
auto z=divp(x,y);
p^=z[0]*q,swap(p,q);
x=y,y=z[1];
}
return {x,p};
}
void read1(poly &x) {
string s; cin>>s;
for(int i=0;i<(int)s.size();++i) if(s[i]=='1') x.set(i);
}
void read2(poly &x) {
int n; cin>>n;
for(int i;n--;) cin>>i,x.set(i-1);
}
signed main() {
ios::sync_with_stdio(false);
int n;
poly S,T,A,B;
cin>>n,read1(S),read1(T),read2(A),read2(B);
A.flip(0),A.flip(n);
auto rs=exgcd(B,A);
poly g=rs[0],q0=rs[1],gs=S%g,gt=T%g,vs=S*q0%A,vt=T*q0%A;
int d=deg(g);
for(int k=1;k<=1000000;++k) {
gs<<=1; if(gs[d]) gs^=g;
vs<<=1; if(vs[n]) vs^=A;
if(gs!=gt) continue;
poly q=vs^vt;
if(deg(q)-d<k) {
q=q/g;
cout<<k<<"\n"; for(int i=k-1;~i;--i) cout<<(i<n&&q[i]); cout<<"\n";
return 0;
}
}
cout<<"-1\n";
return 0;
}
Round #32 - 2024.11.18
G. Guess One Character
题目大意
给定 01 串 \(s\),\(3\) 次交互询问某个 01 串 \(t\) 在 \(s\) 中作为子串的出现次数,求出 \(s\) 中的任意一个位置。
数据范围:\(n\le 50\)。
思路分析
将序列看成若干交替的 \(0,1\) 连续段,取 \(t=0,t=00\) 两者之差即为 \(0\) 连续段个数。
与 \(t=01\) 的出现次数比较即可知道 \(s_n\) 是 \(0\) 还是 \(1\)。
时间复杂度 \(\mathcal O(1)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
int q(string s) {
cout<<"1 "<<s<<endl;
int x; cin>>x; return x;
}
void solve() {
int n;
cin>>n;
int sg=q("0")-q("00"),jp=q("01");
cout<<"0 "<<n<<" "<<(jp==sg)<<endl;
int _; cin>>_;
}
signed main() {
int T; cin>>T;
while(T--) solve();
return 0;
}
F. Alternative Platforms
题目大意
给定 \(a_1\sim a_n,b_1\sim b_n\),对于每个 \(k\),随机选出一个大小为 \(k\) 的集合 \(S\),求 \(\max(\min_{i\in S}a_i,\min_{i\in S} b_i)\)。
数据范围:\(n\le 2\times 10^5\)。
思路分析
容斥把所求转成 \(\min_{i\in S}a_i+\min_{i\in S} b_i-\min(\min_{i\in S}a_i,\min_{i\in S} b_i)\),三部分分别计算。
第三部分可以看成 \(\min _{i\in S} \min(a_i,b_i)\),因此三个部分等价,相当于求 \(\min a_i\) 的期望 \(f_k\)。
对 \(i\) 排序,枚举最大值得到 \(f_k=\sum_{i\ge k}a_i\binom{i-1}{k-1}\),NTT 计算。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MOD=998244353,N=1<<19,G=3;
int rev[N],inv[N],fac[N],ifac[N],w[N<<1];
int ksm(int a,int b=MOD-2) {
int ret=1;
for(;b;a=1ll*a*a%MOD,b=b>>1) if(b&1) ret=1ll*ret*a%MOD;
return ret;
}
void poly_init() {
inv[1]=1;
for(int i=2;i<N;++i) inv[i]=1ll*(MOD-MOD/i)*inv[MOD%i]%MOD;
fac[0]=ifac[0]=1;
for(int i=1;i<N;++i) fac[i]=1ll*fac[i-1]*i%MOD,ifac[i]=1ll*ifac[i-1]*inv[i]%MOD;
for(int k=1;k<=N;k<<=1) {
int x=ksm(G,(MOD-1)/k); w[k]=1;
for(int i=1;i<k;++i) w[i+k]=1ll*x*w[i+k-1]%MOD;
}
}
int plen(int x) { int y=1; for(;y<x;y<<=1); return y; }
void ntt(int *f,bool idft,int n) {
for(int i=0;i<n;++i) {
rev[i]=(rev[i>>1]>>1);
if(i&1) rev[i]|=n>>1;
}
for(int i=0;i<n;++i) if(rev[i]<i) swap(f[i],f[rev[i]]);
for(int k=2,x,y;k<=n;k<<=1) {
for(int i=0;i<n;i+=k) {
for(int j=i;j<i+k/2;++j) {
x=f[j],y=1ll*f[j+k/2]*w[k+j-i]%MOD;
f[j]=(x+y>=MOD)?x+y-MOD:x+y,f[j+k/2]=(x>=y)?x-y:x+MOD-y;
}
}
}
if(idft) {
reverse(f+1,f+n);
for(int i=0,x=ksm(n);i<n;++i) f[i]=1ll*f[i]*x%MOD;
}
}
void poly_mul(const int *f,const int *g,int *h,int n,int m) {
static int a[N],b[N];
for(int i=0;i<n;++i) a[i]=f[i];
for(int i=0;i<m;++i) b[i]=g[i];
int len=plen(n+m-1);
ntt(a,0,len),ntt(b,0,len);
for(int i=0;i<len;++i) h[i]=1ll*a[i]*b[i]%MOD;
ntt(h,1,len);
memset(a,0,sizeof(int)*len);
memset(b,0,sizeof(int)*len);
}
int C(int x,int y) { return 1ll*fac[x]*ifac[y]%MOD*ifac[x-y]%MOD; }
int n,a[N],b[N],v[N],f[N],g[N],h[N];
signed main() {
poly_init();
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=1;i<=n;++i) scanf("%d",&b[i]),v[i]=min(a[i],b[i]);
sort(a+1,a+n+1,greater<>()),sort(b+1,b+n+1,greater<>()),sort(v+1,v+n+1,greater<>());
for(int i=1;i<=n;++i) f[i]=1ll*(a[i]+b[i]-v[i])*fac[i-1]%MOD;
for(int i=0;i<=n;++i) g[n-i]=ifac[i];
poly_mul(f,g,h,n+1,n+1);
for(int i=1;i<=n;++i) printf("%lld ",1ll*h[i+n]*ifac[i-1]%MOD*ksm(C(n,i))%MOD); puts("");
return 0;
}
D. Divide OR Conquer
题目大意
给定 \(a_1\sim a_n\),将其划分成若干段子区间,使得从左到右每一段内元素的 OR 和递增。
数据范围:\(n\le 2\times 10^5,a_i\le 10^9\)。
思路分析
显然 dp,\(f_{i,v}\) 表示划分 \(a[1,i]\),最后一段右端点为 \(i\) 的方案数。
根据经典结论,对于每个 \(i\),只有 \(\mathcal O(\log V)\) 个有效的 \(v\)。
转移为 \(f_{i,v}\gets f_{j,x}\),限制为 \(j\in[l_{i,v},r_{i,v}],x\le v\),其中 \(l,r\) 是异或和为 \(v\) 的后缀的左端点范围。
按 \(v\) 排序后树状数组维护每个 \(i\) 上的 \(\sum_v f_{i,v}\) 即可快速转移。
时间复杂度 \(\mathcal O(n\log V\log n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,V=(1<<30)-1,MOD=998244353;
inline void addv(int &x,const int &y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
int n,a[MAXN],st[MAXN][20],rt[MAXN];
int bit(int x) { return 1<<x; }
int qry(int l,int r) {
int k=__lg(r-l+1);
return st[l][k]|st[r-bit(k)+1][k];
}
struct FenwickTree {
int tr[MAXN],s;
void add(int x,int v) { for(++x;x<=n+1;x+=x&-x) addv(tr[x],v); }
int qry(int x) { for(s=0;x;x&=x-1) addv(s,tr[x]); return s; }
int qry(int l,int r) { return (qry(r+1)+MOD-qry(l))%MOD; }
} T;
struct seg {
int l,r,x,v;
};
int pre(int k,int R) {
int l=1,r=k,p=k,w=qry(k,R);
while(l<=r) {
int mid=(l+r)>>1;
if(qry(mid,R)==w) p=mid,r=mid-1;
else l=mid+1;
}
return p;
}
signed main() {
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i],st[i][0]=a[i];
for(int k=1;k<20;++k) for(int i=1;i+bit(k)-1<=n;++i) {
st[i][k]=st[i][k-1]|st[i+bit(k-1)][k-1];
}
vector <seg> Q;
Q.push_back({0,0,0,0});
for(int i=1;i<=n;++i) {
for(int j=i;j;) {
int k=pre(j,i);
Q.push_back({k-1,j-1,i,qry(j,i)}),j=k-1;
}
}
sort(Q.begin(),Q.end(),[&](auto i,auto j){
return i.v^j.v?i.v<j.v:i.x<j.x;
});
for(auto q:Q) {
int val=q.x?T.qry(q.l,q.r):1;
T.add(q.x,val);
}
printf("%d\n",T.qry(n,n));
return 0;
}
M. Royal Flush
题目大意
给定 \(n\) 种花色,每种花色 \(13\) 张牌,初始你有五张牌,每轮你可以弃置手中的若干张牌,抽取等量的牌,目标是让你的手牌为同一种花色的 \(1,2,3,4,5\),求最优策略下的期望轮数。
数据范围:\(n\le 4\)。
思路分析
直接爆搜,进行一定的剪枝:
-
点数不为 \(1,2,3,4,5\) 的牌拿到就会立刻弃置,只关心这种牌的种数。
-
注意到某种花色中的 \(1,2,3,4,5\) 如果被弃置了一张,剩余的牌此后拿到也会立刻弃置。
因此每种花色只有两种状态:不可能凑出同花顺,或还剩 \(i\) 张牌在牌堆,手上有 \(5-i\) 张牌。
转移的时候搜索求出每种牌弃置了几张,以及每种牌抽到了几张,可以很快得出结果。
回答答案时打表,时间复杂度 \(\mathcal O(1)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
#define ld long double
using namespace std;
const int n=4; //change this
const ld inf=1e18;
typedef array<int,n> info;
map <pair<int,info>,ld> DP;
ll C[60][60];
ld f(int m,info a) {
if(*max_element(a.begin(),a.end())==-1) return inf;
if(*max_element(a.begin(),a.end())==5) return 0;
if(DP.count({m,a})) return DP[{m,a}];
int o=5,x[n+5],y[n+5];
for(int i=0;i<n;++i) if(~a[i]) o-=a[i];
ld S=inf;
for(int s=0;s<(1<<n);++s) {
bool ok=1;
for(int i=0;i<n;++i) if(s>>i&1) ok&=a[i]>0;
if(!ok) continue;
ld val=0; int cnt=o;
y[n]=m;
for(int i=0;i<n;++i) if(~a[i]) {
if(s>>i&1) y[i]=0,cnt+=a[i];
else y[i]=5-a[i],y[n]-=y[i];
} else y[i]=0;
if(!cnt||cnt>m) continue;
function<void(int,int)> dfs=[&](int p,int r) {
if(p==n) {
if(r>y[n]) return ;
x[n]=r;
ld pr=1;
info b; b.fill(0);
for(int i=0;i<n;++i) if(~a[i]) {
if(s>>i&1) b[i]=-1;
else b[i]=x[i]+a[i];
} else b[i]=-1;
for(int i=0;i<=n;++i) {
pr*=C[y[i]][x[i]];
}
pr/=C[m][cnt];
val+=pr*f(m-cnt,b);
return ;
}
for(x[p]=0;x[p]<=min(y[p],r);++x[p]) dfs(p+1,r-x[p]);
return ;
};
dfs(0,cnt);
S=min(S,val);
}
return DP[{m,a}]=S+1;
}
signed main() {
for(int i=0;i<60;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=C[i-1][j]+C[i-1][j-1];
int x[n+5],y[n+5];
for(int i=0;i<n;++i) y[i]=5;
y[n]=8*n;
ld val=0;
function<void(int,int)> dfs=[&](int p,int r) {
if(p==n) {
if(r>y[n]) return ;
x[n]=r;
ld pr=1;
info b; b.fill(0);
for(int i=0;i<n;++i) b[i]=x[i];
for(int i=0;i<=n;++i) {
pr*=C[y[i]][x[i]];
}
pr/=C[n*13][5];
val+=pr*f(n*13-5,b);
return ;
}
for(x[p]=0;x[p]<=min(y[p],r);++x[p]) dfs(p+1,r-x[p]);
return ;
};
dfs(0,5);
printf("%.20Lf\n",val);
return 0;
}
*H. Galactic Council
题目大意
给定 \(n\) 个人,\(m\) 轮,每轮可以投票给当前得票非最多的人,第 \(i\) 轮投票给 \(j\) 有 \(c_{i,j}\) 收益。
已知每轮投票后得票最多的人,构造方案最大化收益。
数据范围:\(n,m\le 50\)。
思路分析
由于我们不会给当前最多票的人投票,因此每个时刻票数最多的人的得票数已知,即每个时刻每个人的票数上限已知。
可以用网络流描述,\((i,j)\to (i,j+1)\) 的流量表示第 \(i\) 个人第 \(j\) 个时刻的票数,每轮建一个虚拟点加流量,求出最大费用最大流。
由于我们要当前钦定票数最多的人的得票数,因此要做上下界最大费用最大流。
时间复杂度 \(\mathcal O(n^2m^3)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
namespace F {
const int MAXV=5005,MAXE=3e5+5;
struct Edge { int v,e,f,w; } G[MAXE];
int S,T,ec=1,hd[MAXV],dis[MAXV],pre[MAXV];
bool inq[MAXV];
void adde(int u,int v,int f,int w) { G[++ec]={v,hd[u],f,w},hd[u]=ec; }
void link(int u,int v,int f,int w) { adde(u,v,f,w),adde(v,u,0,-w); }
bool SPFA() {
memset(dis,0x3f,sizeof(dis));
memset(pre,0,sizeof(pre));
memset(inq,false,sizeof(inq));
queue <int> Q; Q.push(S),inq[S]=true,dis[S]=0;
while(Q.size()) {
int u=Q.front(); Q.pop(),inq[u]=false;
for(int i=hd[u],v;i;i=G[i].e) if(G[i].f&&dis[v=G[i].v]>dis[u]+G[i].w) {
dis[v]=dis[u]+G[i].w,pre[v]=i;
if(!inq[v]) Q.push(v),inq[v]=true;
}
}
return pre[T];
}
array<int,2> ssp() {
int f=0,c=0;
while(SPFA()) {
int g=inf;
for(int u=T;u!=S;u=G[pre[u]^1].v) g=min(g,G[pre[u]].f);
f+=g,c+=g*dis[T];
for(int u=T;u!=S;u=G[pre[u]^1].v) G[pre[u]].f-=g,G[pre[u]^1].f+=g;
}
return {f,c};
}
}
const int MAXN=55,MAXV=5005;
int n,m,s,t,a[MAXN],cl[MAXN],w[MAXN][MAXN],tot,id[MAXN][MAXN],deg[MAXV],e[MAXN][MAXN];
int link(int u,int v,int l,int r,int c) {
deg[u]-=l,deg[v]+=l; if(l<r) F::link(u,v,r-l,c);
return F::ec;
}
signed main() {
scanf("%d%d",&n,&m),s=++tot,t=++tot;
for(int i=1;i<=m;++i) scanf("%d",&cl[i]),a[i]=a[i-1]+(cl[i]>cl[i-1]);
for(int i=1;i<=n;++i) {
for(int j=1;j<=m;++j) id[i][j]=++tot;
id[i][m+1]=t;
}
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) {
if(i==cl[j]) link(id[i][j],id[i][j+1],a[j],a[j],0);
else link(id[i][j],id[i][j+1],0,a[j]-(i<cl[j]),0);
}
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) scanf("%d",&w[i][j]);
for(int j=1;j<=m;++j) {
int nw=++tot; link(s,nw,1,1,0);
for(int i=1;i<=n;++i) if(i!=cl[j-1]) e[i][j]=link(nw,id[i][j],0,1,-w[i][j]);
}
int vs=F::S=tot+1,vt=F::T=tot+2,trg=0;
for(int i=1;i<=tot;++i) {
if(deg[i]<0) F::link(i,vt,-deg[i],0);
else if(deg[i]) trg+=deg[i],F::link(vs,i,deg[i],0);
}
F::link(t,s,inf,0);
if(F::ssp()[0]!=trg) return puts("-1"),0;
for(int i=1;i<=tot+2;++i) F::hd[i]=F::G[F::hd[i]].e;
F::S=s,F::T=t,F::ssp();
for(int j=1;j<=m;++j) {
for(int i=1;i<=n;++i) if(F::G[e[i][j]].f) printf("%d ",i);
}
puts("");
return 0;
}

浙公网安备 33010602011771号