2019 Multi-University Training Contest 1
2019 Multi-University Training Contest 1
A
题意:给长度为 n 的序列染 4 种颜色,有 m 个限制,每个形如 \([l_i,r_i]\) 区间内恰好有 \(x_i\) 种颜色。问合法方案数。 \(n,m \le 100\)
key:dp
其实思路很简单,主要问题在于如何记录状态才能表示出这个限制。考虑一个区间内的颜色种数怎么表示:首先这个区间内必须已经染色,其次每种颜色只贡献 1 。后者可以用该颜色最后一次出现位置表示。
所以 \(f_{i,j,k,l}\) 表示四种颜色最后一次出现的位置。为了表示方便可以给他们排个序:为四种颜色最后一次出现位置排序后分比为 \(i, j, k, l, \ i<j<k<l\) 。这样每次就可以检查以 l 为结尾的所有区间是否合法了。 复杂度 \(O(n^4+n^3m)\)
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef long double LD;
typedef pair<int,int> pii;
typedef pair<LL,int> pli;
const int SZ = 8e6 + 10;
const int INF = 1e9 + 10;
const int mod = 998244353;
const LD eps = 1e-8;
LL read() {
LL n = 0;
char a = getchar();
bool flag = 0;
while(a > '9' || a < '0') { if(a == '-') flag = 1; a = getchar(); }
while(a <= '9' && a >= '0') { n = n * 10 + a - '0',a = getchar(); }
if(flag) n = -n;
return n;
}
int f[2][110][110][110];
vector<pii> q[110];
bool check(int i,int j,int k,int t) {
for(pii p : q[t]) {
int l = p.first,x = p.second;
//cout << l << " " << x << endl;
if(l<=i) if(x!=4) return false; else continue;
if(l<=j) if(x!=3) return false; else continue;
if(l<=k) if(x!=2) return false; else continue;
if(l<=t) if(x!=1) return false; else continue;
}
return true;
}
int main() {
int T = read();
while(T --) {
int n = read(),m = read();
for(int i = 1;i <= n;i ++) q[i].clear();
for(int i = 1;i <= m;i ++) {
int l = read(),r = read(),x = read();
q[r].push_back(make_pair(l,x));
}
// cout << check(0,1,2,3) << endl;
memset(f,0,sizeof f);
f[0][0][0][0] = 1;
for(int t = 0,cur = 0;t < n;t ++,cur^=1) {
for(int k = 0;k <= max(t-1,0);k ++)
for(int j = 0;j <= max(k-1,0);j ++)
for(int i = 0;i <= max(j-1,0);i ++)
f[cur^1][i][j][k] = 0;
for(int k = 0;k <= max(t-1,0);k ++) {
for(int j = 0;j <= max(k-1,0);j ++) {
for(int i = 0;i <= max(j-1,0);i ++) {
if(check(i,j,k,t)) {
// printf("(%d,%d,%d,%d) = %d\n",i,j,k,t,f[cur][i][j][k]);
(f[cur^1][j][k][t] += f[cur][i][j][k]) %= mod;
(f[cur^1][i][k][t] += f[cur][i][j][k]) %= mod;
(f[cur^1][i][j][t] += f[cur][i][j][k]) %= mod;
(f[cur^1][i][j][k] += f[cur][i][j][k]) %= mod;
}
// else printf("!(%d,%d,%d,%d) = %d\n",i,j,k,t,f[cur][i][j][k]);
}
}
}
}
int ans = 0;
for(int k = 0;k <= max(n-1,0);k ++) {
for(int j = 0;j <= max(k-1,0);j ++) {
for(int i = 0;i <= max(j-1,0);i ++) {
if(check(i,j,k,n)) {
(ans += f[n&1][i][j][k]) %= mod;
}
}
}
}
printf("%d\n",ans);
}
}
/**
2
4 1
1 3 3
*/
B
题意:给一个序列,每次操作是尾部加入一个数、查询区间内的线性基。强制在线。 \(n,m \le 2*10^5\)
key:线性基
CF1100F
C
D
E
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef long double LD;
typedef pair<int,int> pii;
typedef pair<LL,int> pli;
const int SZ = 8e6 + 10;
const int INF = 1e9 + 10;
const int mod = 998244353;
const LD eps = 1e-8;
LL read() {
LL n = 0;
char a = getchar();
bool flag = 0;
while(a > '9' || a < '0') { if(a == '-') flag = 1; a = getchar(); }
while(a <= '9' && a >= '0') { n = n * 10 + a - '0',a = getchar(); }
if(flag) n = -n;
return n;
}
vector<pii> g[SZ];
LL dist[SZ];
bool vis[SZ];
int n,m;
void dij(int s) {
for(int i = 1;i <= n;i ++) dist[i] = 1e18,vis[i] = 0;
dist[s] = 0;
priority_queue<pli> q; q.push(make_pair(0,s));
while(q.size()) {
int u = q.top().second; q.pop();
if(vis[u]) continue;
vis[u] = 1;
for(pii e : g[u]) {
int v = e.first;
if(dist[v] > dist[u] + e.second) {
dist[v] = dist[u] + e.second;
q.push(make_pair(-dist[v],v));
}
}
}
}
struct edge {
int f,t;
LL d;
}l[SZ];
int head[SZ],nxt[SZ],tot = 1;
void build(int f,int t,LL d) {
l[++ tot] = (edge){f,t,d};
nxt[tot] = head[f];
head[f] = tot;
}
void insert(int f,int t,LL d) {
build(f,t,d); build(t,f,0);
}
int deep[SZ];
bool bfs(int s,int e) {
for(int i = 1;i <= n;i ++) deep[i] = 0;
deep[s] = 1;
queue<int> q; q.push(s);
while(q.size()) {
int u = q.front(); q.pop();
for(int i = head[u];i;i = nxt[i]) {
int v = l[i].t;
if(l[i].d && !deep[v]) {
deep[v] = deep[u] + 1;
q.push(v);
if(v == e) return true;
}
}
}
return false;
}
LL dfs(int u,LL flow,int e) {
if(u == e || flow == 0) return flow;
LL ans = flow;
for(int i = head[u];i;i = nxt[i]) {
int v = l[i].t;
if(l[i].d && deep[v] == deep[u] + 1) {
LL f = dfs(v,min(ans,l[i].d),e);
if(f > 0) {
l[i].d -= f; l[i^1].d += f;
ans -= f;
if(ans == 0) break;
}
else deep[v] = 0;
}
}
if(ans == flow) deep[u] = 0;
return flow - ans;
}
LL dinic(int s,int e) {
LL ans = 0;
while(bfs(s,e)) {
LL tmp = dfs(s,1e18,e);
if(tmp == 0) break;
ans += tmp;
}
return ans;
}
int ff[SZ],tt[SZ],dd[SZ];
int main() {
int T = read();
while(T --) {
n = read(),m = read();
for(int i = 1;i <= n;i ++) g[i].clear(),head[i] = 0; tot = 1;
for(int i = 1;i <= m;i ++) {
int x = read(),y = read(),z = read();
g[x].push_back(make_pair(y,z));
ff[i] = x; tt[i] = y; dd[i] = z;
}
dij(1);
if(dist[n] == 1e18) { puts("0"); continue; }
for(int i = 1;i <= m;i ++) {
if(dist[tt[i]] == dist[ff[i]] + dd[i]) {
insert(ff[i],tt[i],dd[i]);
}
}
printf("%lld\n",dinic(1,n));
}
}
F
题意:定义字符串生成操作:每次花费 p 的代价从尾部添加一个字符,或者 q 的代价从尾部添加一个当前字符串的子串。问生成给定字符串的最小代价。 \(\sum |S| \le 5*10^6\)
key:sam
显然是要 dp 的。容易得到一个 dp:添加一个字符,或者从之前某个位置。后者一定是当前字符串后缀是除去它的字符串的一个子串,具体地说,[j+1,i] 是 [1,j] 的一个子串。
这样的 j 可能有很多。由于随着长度的增大,代价也越来越大,所以肯定是维护最小的 j 。考虑已经维护了一个 j ,当添加了一个字符时,要么不动,要么在 sam 上走。所以复杂度是 \(O(n)\)
G
H
I
题意:给一个字符串,求长度恰好为 k ,每个字符出现次数在 \([l_i,r_i]\) 内的字典序最小子序列。 \(\sum |S| \le 3*10^5\)
key:贪心
没有出现次数的限制就是之前计蒜之道的那个题,这个题的限制更强一些。
考虑当前已经构造出了一个前缀,当前位置在 p ,考虑下一个字符应该选哪个。肯定是位置在 p 之后,字典序尽量小,并且选了它之后可以构造出符合要求的答案。
由于同种字母肯定选出现位置最靠前的,所以待选位置至多只有 O(m) 个,而做一次check也是 O(m) 的。所以复杂度是 \(O(nm^2)\) ,其中 m 是字符集,即26
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef long double LD;
typedef pair<int,int> pii;
typedef pair<LL,int> pli;
const int SZ = 1e6 + 10;
const int INF = 1e9 + 10;
const int mod = 998244353;
const LD eps = 1e-8;
LL read() {
LL n = 0;
char a = getchar();
bool flag = 0;
while(a > '9' || a < '0') { if(a == '-') flag = 1; a = getchar(); }
while(a <= '9' && a >= '0') { n = n * 10 + a - '0',a = getchar(); }
if(flag) n = -n;
return n;
}
char a[SZ];
int k,n;
char S[SZ];
int top;
int b[SZ][27],L[27],R[27];
bool check(int a[27],int len,int b[27]) {
int suml = 0,sumr = 0,sum = 0,s = 0;
for(int i = 0;i < 26;i ++) {
if(a[i] + b[i] < L[i]) return false;
if(a[i] < L[i]) {
sum += L[i] - a[i];
}
if(a[i] > R[i]) return false;
s += min(b[i],R[i]-a[i]);
suml += L[i];
sumr += R[i];
}
if(s < len) return false;
if(!(suml <= k && k <= sumr)) return false;
if(sum > len) return false;
return true;
}
char ans[SZ];
int main() {
while(~scanf("%s%d",a,&k)) {
for(int i = 0;i < 26;i ++) L[i] = read(),R[i] = read();
int n = strlen(a);
memset(b[n],0,sizeof b[n]);
vector<int> g[27];
for(int i = 0;i < n;i ++) g[a[i]-'a'].push_back(i);
for(int i = n-1;i >= 0;i --) {
for(int j = 0;j < 26;j ++) b[i][j] = b[i+1][j];
b[i][a[i]-'a'] ++;
}
if(!check(b[n],k,b[0])) { puts("-1"); continue; }
int nowid = -1;
int h[27] = {},num[27] = {};
for(int len = 0;len < k;len ++) {
for(int i = 0;i < 26;i ++) {
while(h[i] < g[i].size() && g[i][h[i]] <= nowid) h[i] ++;
if(h[i] == g[i].size()) continue;
int tmp[27] = {},id = g[i][h[i]];
for(int j = 0;j < 26;j ++) tmp[j] = num[j];
tmp[i] ++;
if(check(tmp,k-len-1,b[id+1])) {
// printf("%d %d %d\n",len,i,id);
nowid = id;
ans[len] = i+'a';
num[i] ++;
break;
}
}
}
for(int i = 0;i < k;i ++) printf("%c",ans[i]); puts("");
}
}
J
K
题意:求
key:推导
主要是傻逼了,卡了奇怪的地方……
枚举三次根号后的那个值,\(\lfloor \sqrt[3]{n} \rfloor\) 特判。很容易推到
然后后面那个看起来很难搞的东西只要展开就好了………………然后就得到了一个 \(O(n^{1/3})\) 的做法
#include<bits/stdc++.h>
using namespace std;
typedef __int128 LL;
typedef long double LD;
typedef pair<int,int> pii;
typedef pair<LL,int> pli;
const int SZ = 1e7 + 10;
const int INF = 1e9 + 10;
const int mod = 998244353;
const LD eps = 1e-8;
template <class T>
void read(T &x) {
static char ch;static bool neg;
for(ch=neg=0;ch<'0' || '9'<ch;neg|=ch=='-',ch=getchar());
for(x=0;'0'<=ch && ch<='9';(x*=10)+=ch-'0',ch=getchar());
x=neg?-x:x;
}
LL sqrt3(LL n) {
LL l = 0,r = 1e7+10;
while(r-l>1) {
LL mid = (l+r) / 2;
if(mid * mid * mid <= n) l = mid;
else r = mid;
}
return l;
}
int phi[SZ];
LL sum[SZ];
int pri[SZ / 10];
bool vis[SZ];
void shai(int n) {
phi[1] = 1;
int tot = 0;
for(int i = 2;i <= n;i ++) {
if(!vis[i]) pri[++ tot] = i,phi[i] = i-1;
for(int j = 1,m;j <= tot && (m=i*pri[j]) <= n;j ++) {
vis[m] = 1;
if(i%pri[j] == 0) {
phi[m] = phi[i] * pri[j];
break;
}
else {
phi[m] = phi[i] * phi[pri[j]];
}
}
}
}
LL ksm(LL a,LL b) {
LL ans = 1;
while(b) {
if(b&1) ans = a * ans % mod;
a = a *a % mod;
b >>= 1;
}
return ans;
}
LL ni6 = ksm(6,mod-2);
LL ni2 = ksm(2,mod-2);
LL f2(LL n) {
return n * (n+1) * (2*n+1) / 6;
}
LL f1(LL n) {
return n * (n+1) / 2;
}
int baoli(int n) {
int ans = 0;
for(int i = 1;i <= n;i ++) {
int x = (int)(pow(i,1.0/3)+1e-6);
ans += __gcd(x,i);
}
return ans;
}
int main() {
shai(1e7);
int T; read(T);
while(T --) {
LL n; read(n);
int m = sqrt3(n);
// cout << m << endl;
LL ans = 0;
for(int d = 1;d < m;d ++) {
LL tmp = 3*d*f2((m-1)/d)+ 3*f1((m-1)/d) + ((m-1)/d);
ans += phi[d] * tmp;
}
// cout << ans << endl;
for(int d = 1;d <= m;d ++) {
if(m % d == 0) {
ans += phi[d] * (((n/d) - ((LL)m*m*m-1)/d));
}
}
ans %= mod;
cout << ans << endl;
}
}
L
题意:给一个序列 a ,m 次操作,每次是求 $b_i = \sum_{j = i - k \cdot x} a_j \bmod 998244353 (0 \leq x, 1\leq j \leq i) $ ,然后用 \(b_i\) 替换 \(a_i\) 。求最终序列。 \(n \le 10^5, m \le 10^6, k \in {1,2,3}\)
key:ntt
题解里写的非常清楚…………
首先生成函数分析一下: \(\sum b_ix^i=(\sum x^{ki})(\sum a_ix^i)\) ,所以满足交换律,顺序没有关系。问题在于求 \((\sum x^{ki})^n=\sum {n-1+i \choose i} x^{ki}\) 。然后就做完了。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef long double LD;
typedef pair<int,int> pii;
typedef pair<LL,int> pli;
const int SZ = 2e6 + 10;
const int INF = 1e9 + 10;
const int mod = 998244353;
const LD eps = 1e-8;
LL read() {
LL n = 0;
char a = getchar();
bool flag = 0;
while(a > '9' || a < '0') { if(a == '-') flag = 1; a = getchar(); }
while(a <= '9' && a >= '0') { n = n * 10 + a - '0',a = getchar(); }
if(flag) n = -n;
return n;
}
LL ksm(LL a,LL b) {
LL ans = 1;
while(b) {
if(b&1) ans = a * ans % mod;
a = a *a % mod;
b >>= 1;
}
return ans;
}
struct NTTranform {
const int g = 3;
void Transform(int *a,int n,int opt) {
for(int i = 0,j = 0;i < n;i ++) {
if(i < j) swap(a[i],a[j]);
for(int k = n >> 1;(j ^= k) < k;k >>= 1);
}
for(int l = 2;l <= n;l *= 2) {
int m = l / 2;
int wn = ksm(g,(mod-1)/l);
if(opt == -1) wn = ksm(wn,mod - 2);
for(int *p = a;p != a + n;p += l) {
for(int i = 0,w = 1;i < m;i ++,w=1ll*w*wn%mod) {
int t = 1ll * w * p[m + i] % mod;
p[m + i] = (p[i] - t + mod) % mod;
(p[i] += t) %= mod;
}
}
}
}
void dft(int *a,const int n) {
Transform(a,n,1);
}
void idft(int *a,const int n) {
Transform(a,n,-1);
int t = ksm(n,mod - 2);
for(int i = 0;i < n;i ++) a[i] = 1ll * a[i] * t % mod;
}
}ntt;
void multiply(int *a,int n,int *b,int m,int *ans) { /// need 4 times memory
static int c1[SZ],c2[SZ];
int len = 1;
while(len < n + m) len *= 2;
for(int i = 0;i < len;i ++) c1[i] = c2[i] = 0;
for(int i = 0;i < n;i ++) c1[i] = a[i];
for(int i = 0;i < m;i ++) c2[i] = b[i];
ntt.dft(c1,len); ntt.dft(c2,len);
for(int i = 0;i < len;i ++) c1[i] = 1ll * c1[i] * c2[i] % mod;
ntt.idft(c1,len);
for(int i = 0;i < n + m - 1;i ++) ans[i] = (c1[i] + mod) % mod;
}
int a[SZ],b[SZ],c[SZ],fac[SZ],invfac[SZ];
int C(int n,int m) {
if(n<m) return 0;
return 1ll * fac[n] * invfac[m] % mod * invfac[n-m] % mod;
}
int main() {
fac[0] = 1;
for(int i = 1;i <= 2e6;i ++) fac[i] = 1ll * i * fac[i-1] % mod;
for(int i = 0;i <= 2e6;i ++) invfac[i] = ksm(fac[i],mod-2);
int T = read();
while(T --) {
int n = read(),m = read();
for(int i = 0;i < n;i ++) a[i] = read();
int t[4] = {};
for(int i = 1;i <= m;i ++) t[read()] ++;
for(int k = 1;k <= 3;k ++) {
if(t[k] == 0) continue;
for(int i = 0;i < n;i ++) b[i] = c[i] = 0; b[0] = 1;
int m = t[k];
for(int i = 0;i < n;i += k) c[i] = C(i/k-1+m,i/k);
// for(int i = 0;i < n;i ++) printf("%d ",b[i]); puts("");
// for(int i = 0;i < n;i ++) printf("%d ",c[i]); puts("");
multiply(b,n,c,n,b);
// for(int i = 0;i < n;i ++) printf("%d ",b[i]); puts("");
multiply(b,n,a,n,a);
}
LL ans = 0;
// for(int i = 0;i < n;i ++) printf("%d ",a[i]); puts("");
for(int i = 0;i < n;i ++) {
ans ^= (i+1ll) * a[i];
}
printf("%lld\n",ans);
}
}
/**
2
5 2
3 2 2 4 1
2 2
*/
M
题意:二维平面上给两类点,问是否有一条直线把两类点分开。 \(n \le 100\)
key:计算几何
判断两个凸包是否相离。