2022.7 水题
NWERC2018
B. Brexit Negotiations
题意
有 \(n\) 个项目,每个项目的讨论时间为 \(t_i\),且有若干前置项目(保证前置关系不成环)。每次会议需先对之前结束的每个项目花 \(1\) 单位时间讨论,再选择一个未讨论过的、且前置项目都讨论结束的项目进行讨论。安排会议的顺序使得最长会议的时间最短。\(n\le 4\times 10^5\)。
形式化的,给一个点权为 \(e(i)\) 的 DAG,求一个拓扑序 \(\pi\),最小化
题解
简单题。考虑对 DAG 以贪心的顺序进行拓扑排序。可以将原图进行反向拓扑排序求出每个点后继点的最大点权 \(mx_i\),然后按照 \(mx_i\) 从大到小的顺序进行拓扑排序即可,用堆维护。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5;
vector<int> e[N],re[N];
int n,a[N],in[N],rin[N],mx[N];
queue<int> q;
priority_queue<pair<int,int>> pq;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
scanf("%d%d",&a[i],&in[i]);
mx[i]=a[i];
for(int j=1;j<=in[i];++j)
{
int x; scanf("%d",&x);
e[x].push_back(i);
re[i].push_back(x);
++rin[x];
}
}
for(int i=1;i<=n;++i) if(!rin[i]) q.push(i);
while(!q.empty())
{
int u=q.front(); q.pop();
for(auto v:re[u])
{
mx[v]=max(mx[v],mx[u]);
if(!(--rin[v])) q.push(v);
}
}
for(int i=1;i<=n;++i) if(!in[i]) pq.push(make_pair(mx[i],i));
int ans=0, cnt=0;
while(!pq.empty())
{
int u=pq.top().second; pq.pop();
ans=max(ans,(cnt++)+a[u]);
for(auto v:e[u])
if(!(--in[v])) pq.push(make_pair(mx[v],v));
}
printf("%d\n",ans);
}
F. Fastest Speedrun
题意
你要通关一个游戏,有 \(n\) 关,初始时你的武器等级为 \(0\),用等级为 \(j\) 的武器通过第 \(i\) 关耗费时间为 \(a_{i,j}\),通关后课获得等级为 \(i\) 的武器。每个关还有隐藏捷径:第 \(i\) 关用等级为 \(x_i\) 的武器耗费时间为 \(s_i\)。保证 \(a_{i,0}\ge a_{i,1} \ge\dots\ge a_{i,n}\ge s_i\)。安排通关顺序使得通过所有 \(n\) 关的时间最短。
\(n\le 2500\),CF 上 \(n\) 最大只有 \(250\) 不知何故。
题解
将 \(s_i\) 加到答案里,并将 \(a_{i,j}\) 减去 \(s_i\),这样走隐藏路径视为免费。
不难发现隐藏路径 \(x_i\to i\) 连边后构成基环外向森林,对于每棵基环树通过环上任意一点即可解决整棵树。用类似的方法,找到每个环上最小的 \(a_{i,n}\) 加到答案里,并将环上所有 \(a_{i,j}\) 减去该值,这样走到 \(n\) 点即可解决所有的点。
问题转变为求 \(0\to n\) 的最短路,将所有 \(a_{i,j}\) 和隐藏路径的零边建图直接跑最短路即可。
代码
找环偷懒直接写了个 tarjan
#include<bits/stdc++.h>
using namespace std;
const int N=2505;
typedef long long ll;
vector<int> cir,te[N];
typedef pair<int,int> pr;
vector<pr> e[N];
priority_queue<pr,vector<pr>,greater<pr>> q;
int n,a[N][N],dfn[N],low[N],tid,st[N],tp,bid,id[N],fa[N];
bool ins[N],sc[N];
ll dis[N],ans,tag[N];
void solve()
{
if(cir.size()==1&&!sc[cir[0]]) return ;
int id=cir[0];
for(auto x:cir) if(a[id][n]>a[x][n]) id=x;
ans+=a[id][n];
tag[bid]+=a[id][n];
}
void tarjan(int u)
{
dfn[u]=low[u]=++tid;
st[++tp]=u, ins[u]=true;
for(auto v:te[u])
{
if(!dfn[v]) tarjan(v), low[u]=min(low[u],low[v]);
else if(ins[v]) low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++bid;
cir.clear();
int v;
do
{
v=st[tp--];
id[v]=bid;
ins[v]=false;
cir.push_back(v);
} while(tp&&v!=u);
solve();
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
int x,s;
scanf("%d%d",&x,&s);
ans+=s;
if(x==i) sc[i]=true;
fa[i]=x, te[x].push_back(i);
for(int j=0;j<=n;++j)
{
scanf("%d",&a[i][j]);
a[i][j]-=s;
}
}
for(int i=0;i<=n;++i) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;++i)
{
e[fa[i]].push_back(pr(0,i));
for(int j=0;j<=n;++j) e[j].push_back(pr(a[i][j]-tag[id[i]],i));
}
memset(dis,0x3f,sizeof(dis));
dis[0]=0, q.push(pr(0,0));
while(!q.empty())
{
int u=q.top().second, t=q.top().first;
q.pop();
if(dis[u]!=t) continue;
for(auto x:e[u])
{
int v=x.second;
if(dis[v]>dis[u]+x.first)
{
dis[v]=dis[u]+x.first;
q.push(pr(dis[v],v));
}
}
}
printf("%lld\n",dis[n]+ans);
}
NWERC2020
F. Flight Collision
题意
数轴上有 \(n\) 个无人机,位于 \(x_i\) 的无人机速度为 \(v_i\),两个无人机相遇于一点会相撞坠落,求哪些无人机永远不会坠落。\(n\le 10^5\)。保证不会有三个及以上无人机相撞。
题解
简单题。用 set 维护未坠落无人机下标,可删堆维护每个无人机与前面无人机相撞的时间,每次挑出时间最短的一对扔掉。随便维护一下即可。
代码
注意开 ld,不然过不去
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long double ld;
int x[N],v[N],n;
typedef pair<double,int> pr;
set<int> id;
set<pr> s;
ld d[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i) id.insert(i),scanf("%d%d",&x[i],&v[i]);
for(int i=1;i<n;++i)
{
if(v[i+1]<v[i])
{
d[i]=(ld)(x[i+1]-x[i])/(v[i]-v[i+1]);
s.insert(make_pair(d[i],i));
}
}
while(!s.empty())
{
auto it=s.begin();
int i=it->second;
s.erase(it);
if(id.find(i)==id.end()) continue;
auto pre=id.find(i),nxt=pre; ++nxt; ++nxt;
if(pre!=id.begin())
{
--pre;
s.erase(make_pair(d[*pre],*pre));
if(nxt!=id.end()&&v[*nxt]<v[*pre])
{
d[*pre]=(ld)(x[*nxt]-x[*pre])/(v[*pre]-v[*nxt]);
s.insert(make_pair(d[*pre],*pre));
}
}
--nxt;
id.erase(*nxt);
id.erase(i);
}
printf("%d\n",id.size());
for(auto x:id) printf("%d ",x);
}
G. Great Expectations
题意
你要速通一个游戏,你的最快通关时间为 \(n\) 秒,需要在小于 \(r\) 秒的时间内通关。游戏中有 \(m\) 个陷阱,第 \(i\) 个陷阱在第 \(t_i\) 秒的时间出现,你通过该陷阱的概率为 \(p_i\),如果未通过陷阱会增加 \(d_i\) 的罚时。
你可以在任意时间选择重新开始游戏。你需要最小化通关所需总的期望时间。
\(n<r\le 5000, m\le 50\)。
题解
在未通过陷阱时有两种策略:重开或者继续游戏。
考虑期望 DP,设 \(f_{i,j}\) 表示在第 \(i\) 个陷阱,罚时为 \(j\) 时通关所需的最小期望时间,有
发现这里有个 \(f_{0,0}\) 未知,转移成环了。我们考虑直接二分 \(f_{0,0}\),以最后求出的 \(f_{0,0}\) 与二分的值的大小关系来调整上下界。
代码
by 牛逼队友
#include <cstdio>
#include <iostream>
#include <unordered_map>
#include <ctime>
#define Rep(i, n) for (int i = 1; i <= n; i ++)
#define Rep0(i, n) for (int i = 0; i <= n; i ++)
using namespace std;
const int N = 5010;
int n, r, m;
double p[N];
int t[N], d[N];
double f[N][N];
double calc(double s)
{
t[m + 1] = n;
for (int i = m; i >= 0; i --) Rep0(j, r - 1) {
double tmp = 0;
if (j + d[i] >= r - n) tmp = s;
else {
tmp = min(s, f[i + 1][j + d[i]] + d[i] + t[i + 1] - t[i]);
}
f[i][j] = (1.0 - p[i]) * tmp + p[i] * (f[i + 1][j] + t[i + 1] - t[i]);
}
return f[0][0];
}
int main()
{
cin >> n >> r >> m;
Rep(i, m) cin >> t[i] >> p[i] >> d[i];
double l = 0, r = 1e12;
while (r - l > 1e-7) {
double mid = (l + r) / 2;
double ss = calc(mid);
if (ss < mid) r = mid;
else l = mid;
}
cout.setf(ios_base :: fixed);
cout.precision(7);
cout << l << endl;
return 0;
}
NWERC2016
B. British Menu
题意
给定一个 \(n\) 个点 \(m\) 条边的有向图,该图满足每个强连通分量的大小不超过 \(5\),求该图不经过重复点的最长路。\(n\le 10^5, m\le 10^6\).
题解
模拟题,对于每个强连通分量,将所有点对拆出来建新图,新图点权即为点对之间的最长路(可用暴搜求出),然后求 DAG 上最长路即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,m,dfn[N],low[N],tid,bid,id[N],iid[N],st[N],tp,f[7][7],g[7],an,w[N],ind[N],mx[N];
bool ins[N],vis[7];
vector<int> e[N],ne[N],b,in[N],out[N];
queue<int> q;
void dfs(int u, int d, int n)
{
vis[u]=true;
g[u]=max(g[u],d);
for(int v=1;v<=n;++v)
if(!vis[v]&&f[u][v]) dfs(v,d+1,n);
vis[u]=false;
}
void solve()
{
if(b.size()==1)
{
in[b.front()].push_back(b.front());
out[b.front()].push_back(b.front());
return ;
}
memset(f,0,sizeof(f));
for(auto u:b)
for(auto v:e[u]) if(id[u]==id[v]) f[iid[u]][iid[v]]=1;
for(int i=1;i<=b.size();++i)
{
memset(g,0,sizeof(g));
memset(vis,0,sizeof(vis));
dfs(i,0,b.size());
for(int j=1;j<=b.size();++j)
{
w[++an]=g[j]+1;
id[an]=bid;
in[b[i-1]].push_back(an),out[b[j-1]].push_back(an);
}
}
}
void tarjan(int u)
{
dfn[u]=low[u]=++tid;
st[++tp]=u, ins[u]=true;
for(auto v:e[u])
{
if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
else if(ins[v]) low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++bid;
b.clear();
int v,sze=0;
do
{
v=st[tp--], ins[v]=false;
id[v]=bid, iid[v]=++sze;
b.push_back(v);
} while (tp&&u!=v);
solve();
}
}
void create_edge(int u, int v)
{
for(auto i:out[u])
for(auto j:in[v]) if(i!=j)
{
if(id[i]==id[j]&&id[i]) continue;
ne[i].push_back(j);
++ind[j];
}
}
int main()
{
scanf("%d%d",&n,&m); an=n;
for(int i=1;i<=n;++i) w[i]=1;
for(int i=1;i<=m;++i)
{
int u,v;
scanf("%d%d",&u,&v);
if(u!=v) e[u].push_back(v);
}
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
for(int u=1;u<=n;++u)
for(auto v:e[u]) create_edge(u,v);
for(int i=1;i<=an;++i) if(!ind[i]) mx[i]=w[i],q.push(i);
while(!q.empty())
{
int u=q.front(); q.pop();
for(auto v:ne[u])
{
mx[v]=max(mx[v],mx[u]+w[v]);
if(!(--ind[v])) q.push(v);
}
}
int ans=0;
for(int i=1;i<=an;++i) ans=max(ans,mx[i]);
printf("%d\n",ans);
}
Codeforces Educational Round 131
稀烂
D. Permutation Restoration
题意
给定序列 \(\{b_n\}\) 满足
其中 \(\{a_n\}\) 为 \(1\sim n\) 的排列。根据 \(b\) 还原一个可能的 \(a\),保证有解。\(n\le 5\cdot 10^5\).
题解
对于 \(b_i\),可行的 \(a_i\) 的区间为 \([\lfloor\frac{i}{b_i+1}\rfloor+1,\lfloor\frac{i}{b_i}\rfloor]\),问题转变为根据区间构造排列。
将区间按右端点排序,对于每个左端点找一个最小的未被使用的数即可。用 set 或并查集维护。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int a[N],b[N],n,l[N],r[N],id[N];
int main()
{
int T; scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
{
a[i]=0,id[i]=i;
scanf("%d",&b[i]);
l[i]=i/(b[i]+1)+1,r[i]=(b[i]==0?n:i/b[i]);
}
sort(id+1,id+1+n,[&](int x, int y){
if(r[x]==r[y]) return l[x]<l[y];
return r[x]<r[y];
});
set<int> s;
for(int i=1;i<=n;++i) s.insert(i);
for(int i=1;i<=n;++i)
{
auto it=s.lower_bound(l[id[i]]);
a[id[i]]=*it;
s.erase(it);
}
for(int i=1;i<=n;++i) printf("%d ",a[i]); puts("");
}
}
F. Points
题意
维护一个一维数轴,支持加点(不重合)和删点。每次操作后要求输出满足 \(i<j<k, ~k-i\le d\) 的三元组 \((i,j,k)\) 的个数。操作数和值域 \(\le 2\cdot 10^5\) .
题解
对于每个点计算以其为右端点的贡献,设对于第 \(i\) 个点,\([i-d,i-1]\) 内共有 \(a_i\) 个点,则答案为 \(\sum \binom{a_i}{2}\) .
构建线段树,维护 \(\sum a_i\),\(\sum a_i^2\) 和 \(\sum b_i\) ,其中 \(b_i\in\{0,1\}\) 表示当前位置是否存在点。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
typedef long long ll;
int q,d,a[N],n=2e5;
ll st0[N<<2],st1[N<<2],st2[N<<2],tg[N<<2];
#define lx (x<<1)
#define rx (x<<1|1)
void apply(int x, ll w)
{
st2[x]+=w*st1[x]*2+w*w*st0[x];
st1[x]+=w*st0[x];
tg[x]+=w;
}
void pushdown(int x)
{
if(tg[x])
{
apply(lx,tg[x]);
apply(rx,tg[x]);
tg[x]=0;
}
}
void pushup(int x)
{
st0[x]=st0[lx]+st0[rx];
st1[x]=st1[lx]+st1[rx];
st2[x]=st2[lx]+st2[rx];
}
void update(int x, int l, int r, int sl, int sr, ll w)
{
if(sl>sr) return ;
if(sl<=l&&r<=sr)
{
apply(x,w);
return ;
}
pushdown(x);
int mid=l+r>>1;
if(sl<=mid) update(lx,l,mid,sl,sr,w);
if(sr>mid) update(rx,mid+1,r,sl,sr,w);
pushup(x);
}
void update(int x, int l, int r, int s, ll w)
{
if(l==r)
{
if(w>=0)
st0[x]=1,st1[x]=w,st2[x]=w*w;
else
st0[x]=st1[x]=st2[x]=0;
return ;
}
pushdown(x);
int mid=l+r>>1;
s<=mid?update(lx,l,mid,s,w):update(rx,mid+1,r,s,w);
pushup(x);
}
int query(int x, int l, int r, int sl, int sr)
{
if(sl<=l&&r<=sr) return st0[x];
pushdown(x);
int mid=l+r>>1;
if(sr<=mid) return query(lx,l,mid,sl,sr);
if(sl>mid) return query(rx,mid+1,r,sl,sr);
return query(lx,l,mid,sl,sr)+query(rx,mid+1,r,sl,sr);
}
int main()
{
scanf("%d%d",&q,&d);
while(q--)
{
int x; scanf("%d",&x);
if(!a[x])
{
a[x]=1;
int y=query(1,1,n,max(1,x-d),x);
update(1,1,n,x,y);
update(1,1,n,x+1,min(x+d,n),1);
}
else
{
a[x]=0;
update(1,1,n,x,-1);
update(1,1,n,x+1,min(x+d,n),-1);
}
printf("%lld\n",(st2[1]-st1[1])/2);
}
}
ICPC 2019 Nanjing Regional Contest
F. Paper Grading
题意
有 \(n\) 个字符串 \(s_1,s_2,\dots,s_n\) 和 \(m\) 次操作:
1 x y
:交换 \(s_x,s_y\);2 T k l r
:给出串 \(T\),求 \(s_l,s_{l+1},\dots s_r\) 中有多少个串与 \(T\) 的最长公共前缀长度至少为 \(k\)。
\(n\le 2\cdot 10^5\),\(s_i\) 和询问串的串长和各不超过 \(2\cdot 10^5\).
题解
分块+根号重构
对每个块维护一棵 Trie 树,询问能够轻松 O(串长) 解决,对于修改操作根号重构即可,即每根号个操作直接重构,多出的修改直接暴力计算贡献。
总时间复杂度根号级别,好像正解可以做到 log,不过懒得管了。
代码
写了分块,没写过根号重构就丢给牛逼队友写了/kk
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=2e6+5;
int n,m,b,bel[N],br[N],ch[M][26],sum[M],cnt;
string s[N],t;
void ins(int u, string s, int d=1)
{
const int len=s.size();
for(int i=0;i<len;++i)
{
if(!ch[u][s[i]-'a'])
{
ch[u][s[i]-'a']=++cnt;
memset(ch[cnt],0,sizeof(ch[cnt]));
sum[cnt] = 0;
}
u=ch[u][s[i]-'a'];
sum[u]+=d;
}
}
bool chk(string s, string t, int k)
{
if(s.size()<k||t.size()<k) return false;
for(int i=0;i<k;++i) if(s[i]!=t[i]) return false;
return true;
}
int qry(int u, string t, int k)
{
if(t.size()<k) return 0;
for(int i=0;i<k;++i)
{
u=ch[u][t[i]-'a'];
if(!u) return 0;
}
return sum[u];
}
int id[N], top;
int query(string t, int k, int l, int r)
{
int res=0,y;
for(;bel[l]==bel[l-1]&&l<=r;++l) res+=chk(t,s[id[l]],k);
if(l>r) return res;
for(y=bel[l];br[y]+1<=r;++y) res+=qry(y,t,k);
for(l=(y-1)*b+1;l<=r;++l) res+=chk(t,s[id[l]],k);
return res;
}
pair<int, int> st[N];
void rebuild()
{
for(int i=1;i<=bel[n];++i) memset(ch[i],0,sizeof(ch[i]));
cnt=bel[n];
for(int i=1;i<=n;++i) ins(bel[i],s[id[i]]);
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>m;
b=sqrt(n);
for(int i=1;i<=n;++i) bel[i]=(i-1)/b+1;
cnt=bel[n];
for(int i=1;i<=n;++i)
{
cin>>s[i];
ins(bel[i],s[i]);
br[bel[i]]=i, id[i] = i;
}
int bq = sqrt(m);
while(m--)
{
int op; cin>>op;
if(op==1)
{
int x,y;
cin>>x>>y;
st[++ top] = make_pair(x, y);
if (top == bq)
{
for (int i = 1; i <= top; i ++)
swap(id[st[i].first], id[st[i].second]);
rebuild();
top = 0;
}
}
else
{
int k,l,r;
cin>>t>>k>>l>>r;
if(k==0) cout<<r-l+1<<endl;
else {
int tmp = query(t,k,l,r);
for (int i = 1; i <= top; i ++)
{
int x = st[i].first, y = st[i].second;
if (x >= l && x <= r && (y < l || y > r)) {
tmp -= chk(s[id[x]], t, k);
tmp += chk(s[id[y]], t, k);
}
else if (y >= l && y <= r && (x < l || x > r)) {
tmp -= chk(s[id[y]], t, k);
tmp += chk(s[id[x]], t, k);
}
swap(id[x], id[y]);
}
for (int i = top; i; i --) swap(id[st[i].first], id[st[i].second]);
cout<<tmp<<endl;
}
}
}
}
I. Space Station
题意
给定 \(n+1\) 个数 \(a_0,a_1,\cdots a_n\),求有多少个排列 \(p_1,p_2,\dots,p_n\),使得 \(\forall i\in[1,n], a_0+\sum_{j=1}^{i-1} a_{p_j}\le a_{p_i}\)。\(n\le 10^5, 0\le a_i\le 50\).
题解
待填坑
杂题
Gym102994 G. Blackjack
复习下撤销背包,直接抄题解
题意
有 \(n\) 张卡片随机排列,每张卡片有一个分值 \(x_i\)。每次随机抽一张,抽完若卡片分值和在 \((a,b]\) 之间则获胜,超过 \(b\) 则失败。求获胜的概率。\(n,a,b\le 500\) .
题解
直接考虑做排列很困难。考虑转化一下:只需要最后一次选择卡片之前分值和小于等于 \(a\),选择之后恰好在 \((a,b]\) 即可。如此我们只需考虑排列的最后一个元素,以及分值和与概率即可。
令 \(f_{i,j,k}\) 表示前 \(i\) 张卡片选了 \(j\) 张,和为 \(k\) 的概率,转移是背包。
转移还是较为经典的,乘 \(j\) 是考虑选择顺序。DP 复杂度为 \(O(n^3)\)。
枚举排列的最后一个元素,直接 DP 复杂度为 \(O(n^4)\),难以接受,考虑撤销背包,令撤销元素为 \(i\),撤销后背包为 \(g_{j,k}\),则答案为 \(\sum_{k>a-x_i,k\le a,k\le b-x_i} g_{j,k}\cdot \frac{1}{n-j}\)。撤销的复杂度为 \(O(n^2)\),故总时间复杂度仍是 \(O(n^3)\)。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=505;
int n,a,b,x[N];
double f[N][N],g[N][N];
int main()
{
scanf("%d%d%d",&n,&a,&b);
for(int i=1;i<=n;++i) scanf("%d",&x[i]);
f[0][0]=1.0;
for(int i=1;i<=n;++i)
for(int j=i;j;--j)
for(int k=b;k>=x[i];--k)
f[j][k]+=f[j-1][k-x[i]]*j/(n-j+1);
double ans=0;
for(int i=1;i<=n;++i)
{
memcpy(g,f,sizeof(f));
for(int j=1;j<n;++j)
for(int k=x[i];k<=b;++k)
g[j][k]-=g[j-1][k-x[i]]*j/(n-j+1);
for(int j=0;j<n;++j)
for(int k=0;k<=b;++k)
if(k>a-x[i]&&k<=a&&k<=b-x[i])
ans+=g[j][k]/(n-j);
}
printf("%.10f\n",ans);
}
Gym103388 B. Beautiful Words
难以会做,感觉已经完全不会字符串题了 /kk
题意
给定一个长度为 \(n\) 的字符串 \(A\) 和大小为 \(m\) 的字符串集合 \(S\)。定义 \(A\) 的循环排列串 \(B_i=A_iA_{i+1}\dots A_nA_1A_2\dots A_{i-1}\) 的分值为与 \(S\) 中串的最长公共子串的最大长度。求所有 \(B_i\) 的分值的最小值。\(n\le 10^5\)。
题解
将 \(A\) 复制一份,并用 SAM 求出每个前缀所匹配的最长后缀的长度 \(mxlen_i\)。
二分最小分值 \(x\),check 最长满足条件(不包含长度 \(>x\) 的匹配串)的子串长度是否 \(\ge n\)。对于满足 \(mxlen_i> x\) 的点 \(i\) 若被包含进子串,则左端点需满足 \(l\ge i-x+1\)。使用双指针维护每个位置处左端点。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int lst=1,cnt=1,ch[N][27],len[N],pre[N],n,m,mxl[N];
char s[N],t[N];
void extend(int c)
{
int p=lst,np=++cnt;lst=np;
len[np]=len[p]+1;
for(;p&&!ch[p][c];p=pre[p]) ch[p][c]=np;
if(!p) return void(pre[np]=1);
int q=ch[p][c];
if(len[p]+1==len[q]) return void(pre[np]=q);
int nq=++cnt;len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
pre[nq]=pre[q],pre[q]=pre[np]=nq;
for(;ch[p][c]==q;p=pre[p])ch[p][c]=nq;
}
bool check(int x)
{
for(int i=1,j=1;i<=2*n;++i)
{
if(mxl[i]>x)
while(j<i-x+1) ++j;
if(i-j+1>=n) return true;
}
return false;
}
int main()
{
scanf("%d%d%s",&n,&m,s+1);
for(int i=1;i<=n;++i) s[i+n]=s[i];
for(int i=1;i<=m;++i)
{
scanf("%s",t);
for(int j=0;t[j];++j) extend(t[j]-'a');
extend('z'+1-'a');
}
int u=1;
for(int i=1;i<=n*2;++i)
{
mxl[i]=mxl[i-1];
if(ch[u][s[i]-'a']) ++mxl[i],u=ch[u][s[i]-'a'];
else
{
while(u&&!ch[u][s[i]-'a']) u=pre[u];
if(!u) mxl[i]=0,u=1;
else mxl[i]=len[u]+1,u=ch[u][s[i]-'a'];
}
}
int l=0,r=n*2;
while(l<=r)
{
int mid=l+r>>1;
check(mid)?r=mid-1:l=mid+1;
}
printf("%d\n",r+1);
}
EC-Final 2019 E. Flow
题意
给定一个 \(n\) 个点 \(m\) 条边的图,由 \(1\) 到 \(n\) 的若干条长度相等的不相交路径组成。每条边有容量 \(e_i\)。每次可以进行一次操作:将一条边容量减 1,另一条边容量加 1。求最少的操作次数使得 \(1\) 到 \(n\) 的最大流最大。\(n\le 10^5,m\le 2\cdot 10^5\).
题解
简单题,但想了较久(近 2h?)。
转化下题意:给定 \(x\) 个大小均为 \(y\) 的整数集合 \(S_i\),每次可以选任意一个数减 1,另一个数加 1,求最小的操作次数使得所有集合的最小值的和最大。
显然该值可以轻松求出,为所有数的和除以集合的大小(将所有数放入一个集合显然最优)。
根据最终答案和初始答案的差值可以求出每个集合的最小值需要增加的总值。考虑每次选择一个最小值个数最小的集合增加。因为每个集合最小值的个数都是递增的,所以贪心策略合理。用堆维护即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
typedef pair<int,int> pr;
vector<pr> e[N];
vector<vector<int>> v;
vector<int> t;
int n,m,now[N];
priority_queue<pr,vector<pr>,greater<pr>> q;
void dfs(int u)
{
if(u==n)
{
v.push_back(t);
return ;
}
for(auto x:e[u])
{
t.push_back(x.second);
dfs(x.first);
t.pop_back();
}
}
int main()
{
scanf("%d%d",&n,&m);
long long sum=0,ans=0;
for(int i=1;i<=m;++i)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
e[x].push_back(make_pair(y,z));
sum+=z;
}
dfs(1);
int s=v.size(),k=v[0].size();
sum/=k;
for(int i=0;i<s;++i)
{
sort(v[i].begin(),v[i].end());
sum-=v[i].front();
for(now[i]=0;now[i]<k&&v[i][now[i]]==v[i][0];++now[i]);
q.push(pr(now[i],i));
}
while(true)
{
int u=q.top().second; q.pop();
if(now[u]==k)
{
ans+=sum*k;
break;
}
else
{
if(v[u][now[u]]-v[u][now[u]-1]<=sum)
{
ans+=1ll*now[u]*(v[u][now[u]]-v[u][now[u]-1]);
sum-=v[u][now[u]]-v[u][now[u]-1];
int pre=now[u];
for(;now[u]<k&&v[u][now[u]]==v[u][pre];++now[u]);
q.push(pr(now[u],u));
}
else
{
ans+=1ll*now[u]*sum;
break;
}
}
}
printf("%lld\n",ans);
}
EC Final 2020 G. Prof. Pang's sequence
题意
给定一个长度为 \(n\) 的序列 \(a_1,a_2,\dots,a_n\) 和 \(m\) 个询问,每次询问给出 \(l,r\),求 \(a_l,\dots,a_r\) 有多少子段满足不同的数的个数为奇数。
题解
离线扫描线,对每个左端点维护不同的数的个数模 \(2\) 的值,那么添加 \(a_i\) 相当于将 \([pre_{a_i} +1,i]\) 一段答案 \(01\) 反转。求子区间的答案和相当于求区间的历史和。
维护历史和的方法也比较 tricky:每次添加完毕一个数后区间历史和需加上区间内 \(1\) 的个数为贡献。维护懒标记 \(tag_0,tag_1\) 表示区间加的次数,两个标记是因为存在反转操作。
代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,a[N],pre[N],l[N],r[N];
vector<int> qry[N];
typedef long long ll;
ll ans[N];
struct node
{
int s0,s1;
ll sum;
int t0,t1;
bool rev;
} st[N<<2];
#define lx (x<<1)
#define rx (x<<1|1)
void apply_rev(int x)
{
st[x].rev^=1;
swap(st[x].s0,st[x].s1);
swap(st[x].t0,st[x].t1);
}
void apply(int x, int t0, int t1)
{
st[x].t0+=t0, st[x].t1+=t1;
st[x].sum+=1ll*st[x].s0*t0+1ll*st[x].s1*t1;
}
void pushdown(int x)
{
if(st[x].rev)
{
apply_rev(lx),apply_rev(rx);
st[x].rev=false;
}
if(st[x].t0 || st[x].t1)
{
apply(lx,st[x].t0,st[x].t1),apply(rx,st[x].t0,st[x].t1);
st[x].t0=st[x].t1=0;
}
}
void pushup(int x)
{
st[x].s0=st[lx].s0+st[rx].s0;
st[x].s1=st[lx].s1+st[rx].s1;
st[x].sum=st[lx].sum+st[rx].sum;
}
void build(int x, int l, int r)
{
st[x].s0=r-l+1;
if(l==r) return ;
int mid=l+r>>1;
build(lx,l,mid),build(rx,mid+1,r);
}
void update(int x, int l, int r, int sl, int sr)
{
if(sl<=l&&r<=sr)
{
apply_rev(x);
return ;
}
pushdown(x);
int mid=l+r>>1;
if(sl<=mid) update(lx,l,mid,sl,sr);
if(sr>mid) update(rx,mid+1,r,sl,sr);
pushup(x);
}
ll query(int x, int l, int r, int sl, int sr)
{
if(sl<=l&&r<=sr) return st[x].sum;
pushdown(x);
int mid=l+r>>1;
ll res=0;
if(sl<=mid) res=query(lx,l,mid,sl,sr);
if(sr>mid) res+=query(rx,mid+1,r,sl,sr);
return res;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
build(1,1,n);
scanf("%d",&m);
for(int i=1;i<=m;++i)
{
scanf("%d%d",&l[i],&r[i]);
qry[r[i]].push_back(i);
}
for(int i=1;i<=n;++i)
{
update(1,1,n,pre[a[i]]+1,i);
pre[a[i]]=i;
st[1].sum+=st[1].s1, ++st[1].t1;
for(auto x:qry[i])
ans[x]=query(1,1,n,l[x],r[x]);
}
for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
}