NOI.ac2020省选模拟赛3
A.SA
problem
给出一个字符串S,然后有Q次询问,每次询问给出一个字符串T,对于每个询问需要回答:S中有多少个连续子串与T恰好有一个字符不相同。
\(|S|,\sum|T| \le 5\times 10^5\)
solution
先对于S建出后缀数组,然后对于一个询问T。枚举T的一个位置i,然后枚举一下把这个位置改成另外的一个字符c。下面就是在后缀数组中查询新的字符串出现过多少次了。
通过后缀数组可以一个函数\(f(L,R,l,r,len)\)表示在sa中位于区间\([L,R]\)的后缀长度为len的子串,其终点位于原字符串的[l,r]位置的后缀,显然这是一个区间。
然后我们查询这个新的字符串,可以通过查询\([1,i]\)出现的区间和\([i+1,|T|]\)出现的区间,然后合并一下,计算所求的个数。
太难描述了,看代码吧
code
/*
* @Author: wxyww
* @Date: 2020-06-03 08:36:02
* @Last Modified time: 2020-06-03 22:05:14
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<ctime>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 500010;
ll read() {
ll x = 0,f = 1;char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1; c = getchar();
}
while(c >= '0' && c <= '9') {
x = x * 10 + c - '0'; c = getchar();
}
return x * f;
}
int n,sa[N],c[N],x[N],m,rk[N],y[N];
char s[N];
void get_sa() {
for(int i = 1;i <= n;++i) c[x[i] = s[i]]++;
for(int i = 1;i <= m;++i) c[i] += c[i - 1];
for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;
for(int k = 1;k <= n;k <<= 1) {
int num = 0;
for(int i = n - k + 1;i <= n;++i) y[++num] = i;
for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;
for(int i = 2;i <= m;++i) c[i] = 0;
for(int i = 1;i <= n;++i) ++c[x[i]];
for(int i = 1;i <= m;++i) c[i] += c[i - 1];
for(int i = n;i >= 1;--i) sa[c[x[y[i]]]--] = y[i];
swap(x,y);
num = 0;
x[sa[1]] = ++num;
for(int i = 2;i <= n;++i) {
if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = num;
else x[sa[i]] = ++num;
}
if(num == n) break;
m = num;
}
for(int i = 1;i <= n;++i) rk[sa[i]] = i;
}
void get(int L,int R,int LL,int RR,int &x,int &y,int len) {
int l = L,r = R;
x = l,y = r;
if(L > R) return;
x = R + 1,y = L - 1;
while(l <= r) {
int mid = (l + r) >> 1;
if(rk[sa[mid] + len] >= LL) x = mid,r = mid - 1;
else l = mid + 1;
}
l = L,r = R;
while(l <= r) {
int mid = (l + r) >> 1;
if(rk[sa[mid] + len] <= RR) y = mid,l = mid + 1;
else r = mid - 1;
}
}
char t[N];
int L[N],R[N],Lc[N],Rc[N];
int main() {
scanf("%s",s + 1);
n = strlen(s + 1);
m = 'z';
get_sa();
for(int i = 'a';i <= 'z';++i) Lc[i] = n,Rc[i] = 1;
for(int i = 1;i <= n;++i) {
Lc[s[sa[i]]] = min(Lc[s[sa[i]]],i);
Rc[s[sa[i]]] = max(Rc[s[sa[i]]],i);
}
int T = read();
while(T--) {
scanf("%s",t + 1);
int mm = strlen(t + 1);
ll ans = 0;
L[mm + 1] = 0,R[mm + 1] = n;
for(int i = mm;i >= 1;--i)
get(Lc[t[i]],Rc[t[i]],L[i + 1],R[i + 1],L[i],R[i],1);
int tl = 1,tr = n;
for(int i = 1;i <= mm;++i) {
for(int j = 'a';j <= 'z';++j) {
if(j == t[i]) continue;
int x,y;
get(tl,tr,Lc[j],Rc[j],x,y,i - 1);
get(x,y,L[i + 1],R[i + 1],x,y,i);
ans += max(0,y - x + 1);
}
get(tl,tr,Lc[t[i]],Rc[t[i]],tl,tr,i - 1);
}
cout<<ans<<endl;
}
return 0;
}
B.A
problem
在n天完成m项工作,每天可以完成无数项,在第j天完成第i项工作的收益是\(w[i][j]\),\(w[i][j]=-1\)时,表示不能再第j天完成第j项工作。还有K个限制,每个限制给出\(x,y\),需要满足x这项工作在y之前完成。问最大收益。
solution
比较经典的最小割问题。
但是这道题要求的是最大收益,所以把流量取负,然后加上一个定值,最后在减去就行了。
对于每项工作建m条边,对于第x个工作,第i个点向第i+1个点连流量为\(w[x][i]\)的边,割掉这条边表示在第i天完成工作。然后对于每个限制\(x,y\),从x的第i个点向j的第\(i+1\)个点连流量为INF的边。
code
/*
* @Author: wxyww
* @Date: 2020-06-03 09:34:34
* @Last Modified time: 2020-06-03 10:10:55
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<ctime>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 1000010,INF = 1e9;
ll read() {
ll x = 0,f = 1;char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1; c = getchar();
}
while(c >= '0' && c <= '9') {
x = x * 10 + c - '0'; c = getchar();
}
return x * f;
}
struct node {
int v,nxt,w;
}e[N << 1];
int head[N],ejs = 1;
void add(int u,int v,int w) {
// printf("!!%d %d %d\n",u,v,w);
e[++ejs].v = v;e[ejs].nxt = head[u];head[u] = ejs;e[ejs].w = w;
e[++ejs].v = u;e[ejs].nxt = head[v];head[v] = ejs;e[ejs].w = 0;
}
int w[110][110],n,m,K,S,T;
int p(int x,int y) {
return (x - 1) * (m + 1) + y;
}
int dep[N];
queue<int>q;
int bfs() {
memset(dep,0,sizeof(dep));
dep[S] = 1;q.push(S);
// cout<<T<<endl;
while(!q.empty()) {
int u = q.front();q.pop();
for(int i = head[u];i;i = e[i].nxt) {
int v = e[i].v;
if(!dep[v] && e[i].w) {
// if(v == 8) printf("%d\n",u);
dep[v] = dep[u] + 1;q.push(v);
}
}
}
return dep[T];
}
int cur[N];
int dfs(int u,int now) {
if(u == T) return now;
int ret = 0;
for(int &i = cur[u];i;i = e[i].nxt) {
int v = e[i].v;
if(dep[v] == dep[u] + 1 && e[i].w) {
int k = dfs(v,min(e[i].w,now - ret));
e[i].w -= k;e[i ^ 1].w += k;
ret += k;
if(ret == now) return ret;
}
}
return ret;
}
int dinic() {
int ret = 0;
while(bfs()) {
// puts("!!!");
memcpy(cur,head,sizeof(head));
ret += dfs(S,INF);
// cout<<ret<<endl;
}
return ret;
}
int main() {
n = read(),m = read(),K = read();
S = n * (m + 1) + 1,T = S + 1;
for(int i = 1;i <= n;++i) {
for(int j = 1;j <= m;++j) {
int x = read();
add(p(i,j),p(i,j + 1),x == -1 ? INF : 100 - x);
}
}
for(int i = 1;i <= n;++i) {
add(S,p(i,1),INF);
add(p(i,m + 1),T,INF);
}
for(int i = 1;i <= K;++i) {
int x = read(),y = read();
for(int j = 1;j <= m;++j)
add(p(x,j),p(y,j + 1),INF);
}
cout<<100 * n - dinic();
return 0;
}
C.逆序对
problem
给出一个残缺的长度为n的序列,每个位置的数字大小都在\([1,K]\),给残缺的位置填上\([1,k]\)之间的数,使得逆序对数量最大。
\(n\le 2\times 10^5,K\le 100\)
solution
最后脑抽写了个\(n^2\)求原数组逆序对+没开longlong。成功\(100\rightarrow 10\)。。。
首先填的数字肯定是单调不升的。如果有两个位置填的分别是x,y。满足\(x<y\),交换x和y的位置答案肯定不会变劣。
先预处理出\(w[i][j]\)表示前i个残缺的位置全都填j的时候原数组对其产生的逆序对数量。
\(f[i][j]\)表示第i个残缺的位置填的数大于等于j的最大收益。然后我们枚举一下上次填的与这次不同的数的位置k,也就是说区间\([k,j]\)填的全都是j。然后计算产生的逆序对数量,首先肯定要先加上k前面填比j大的数字产生的最大贡献有\(f[k][j+1]\),然后计算一下原数组对\([k,i]\)这段区间填的j产生的贡献,就是\(w[i][j]-w[k][j]\)。然后因为后面填的数字肯定都要比j小,所以还要再加上后面填的数字对\([k,i]\)产生的贡献\((i-k)*(m-i)\)(m是残缺位置的总数量)。
综上,转移方程就是\(f[i][j]=\max\limits_{k=0}^{i-1}f[k][j+1]+w[i][j]-w[k][j]+(i-k)*(m-i)\)
直接转移的复杂度是\(O(n^2 K)\),考虑优化。
将上面的转移方程拆开
\(f[i][j]=\max\limits_{k=0}^{i-1}w[i][j]+im-i^2+f[k][j+1]-w[k][j]-km-ik\)
前面这块\(w[i][j]+im-i^2\)是定值,后面的\(f[k][j+1]-w[k][j]-km\)是\(b\),\(k\)是斜率,\(i\)是x。所以就是\(kx+b\)的形式,\(k\)和\(i\)单调递增,所以用斜率优化将复杂度优化成\(O(nk)\)就行了
code
/*
* @Author: wxyww
* @Date: 2020-06-03 09:05:56
* @Last Modified time: 2020-06-03 19:24:45
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<ctime>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 200004;
#define int ll
ll read() {
ll x = 0,f = 1;char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1; c = getchar();
}
while(c >= '0' && c <= '9') {
x = x * 10 + c - '0'; c = getchar();
}
return x * f;
}
ll w[N][104],f[2][N];
int treel[N],treer[N],n,K,b[N],a[N],m;
void updater(int x,int c) {
while(x <= K) treer[x] += c,++x;
}
void updatel(int x,int c) {
while(x >= 1) treel[x] += c,--x;
}
int q[2][N],H[2],T[2];
ll getb(int k,int j) {
return f[j & 1][k] - w[k][j - 1] - m * k;
}
ll calc(int k,int i,int j) {
ll b = getb(k,j);
return b + i * k;
}
ll check(int k1,int k2,int k3,int j) {
int b1 = getb(k1,j),b2 = getb(k2,j),b3 = getb(k3,j);
return (b3 - b1) * (k1 - k2) < (b2 - b1) * (k1 - k3);
}
signed main() {
n = read(),K = read();
ll ans = 0,ret = 0;
for(int i = 1;i <= n;++i) {
a[i] = read();
if(!a[i]) b[++m] = i;
if(a[i]) {
updater(a[i],1);
}
}
int p = 1;
for(int i = 1;i <= m;++i) {
while(p < b[i]) {
if(!a[p]) {++p;continue;}
ret += treel[a[p] + 1];
updater(a[p],-1);
updatel(a[p],1);
++p;
}
for(int j = 1;j <= K;++j) {
w[i][j] = w[i - 1][j] + treer[j - 1] + treel[j + 1];
}
}
while(p <= n) {
if(!a[p]) {++p;continue;}
ret += treel[a[p] + 1];
updatel(a[p],1);
++p;
}
for(int j = K;j >= 1;--j) {
int t = j & 1,t_1 = t ^ 1;
H[t_1] = 0;T[t_1] = 0;
for(int i = 1;i <= m;++i) {
f[t][i] = f[t_1][i];
while(H[t_1] < T[t_1] && calc(q[t_1][H[t_1]],i,j + 1) < calc(q[t_1][H[t_1] + 1],i,j + 1)) {
++H[t_1];
}
f[t][i] = max(f[t][i],w[i][j] - i * i + m * i + calc(q[t_1][H[t_1]],i,j + 1));
while(H[t_1] < T[t_1] && check(q[t_1][T[t_1] - 1],q[t_1][T[t_1]],i,j + 1)) --T[t_1];
q[t_1][++T[t_1]] = i;
}
ans = max(ans,f[t][m]);
}
cout<<ans + ret<<endl;
return 0;
}