2022 年牛客多校加赛场补题记录

B Bustling City

题意:给定一个 n n n 个点 n n n 条边的有向图,第 i i i 个点恰有一条出边指向 a i a_i ai。在每个点上恰有一个商人,一个时刻位于点 i i i 处的商人会走到 a i a_i ai 号节点,问每个节点第一次有 k k k 个商人同时在该点的时间。 n , k ≤ 1 × 1 0 6 n,k \leq 1\times 10^6 n,k1×106

解法:将图分成若干个连通块,每个连通块由一个环和树根在环上的内向树构成。首先考虑树上的点的答案(排除树根),对于节点 u u u,可以根据它的最大子树深度 h h h 得到一个长度为 h h h 的数组 f u f_u fu,第 x x x 位表示第 x x x 时刻它现在有多少个商人。那么这个信息是可以通过子树合并得到的: f u , i = ∑ v ∈ c h i l d u f v , i − 1 \displaystyle f_{u,i}=\sum_{v \in {\rm child}_u}f_{v,i-1} fu,i=vchildufv,i1。那么就可以进行树上启发式合并,在 O ( n log ⁡ n ) \mathcal O(n \log n) O(nlogn) 的时间得到每个树上节点的答案。

对于环上的节点,考虑维护一个双向环形链表,初始时刻每个点均在环上。依次枚举时刻,再遍历链表,容易计算出该时刻下环上每个点的商人个数。若从某一时刻开始,环上节点 u u u 的子树内再也没有商人进入环中,则可将该点从链表中删除。这样看似暴力的做法复杂度其实仅为各个子树深度之和——每个环上节点只会被遍历它的子树深度层,因而可以 O ( n ) \mathcal O(n) O(n) 的计算环上节点的答案。

因而总的复杂度为 O ( n log ⁡ n + n ) \mathcal O(n \log n + n) O(nlogn+n)

#include
#define IL inline
#define LL long long
#define min(a,b) (a<b?a:b)
using namespace std;
const int N=1e6+3;
struct hh{
	int pre,nxt;
}l[N];
struct kk{
	int to,nxt;
}e[N<<1];
int n,k,to[N],vis[N],bo[N],dep[N],ans[N],val[N],p,id[N];
int buk[N],siz[N],son[N],num,fir[N];
vector<int>c,d[N];
IL int in(){
  char c;int f=1;
  while((c=getchar())<'0'||c>'9')
    if(c=='-') f=-1;
  int x=c-'0';
  while((c=getchar())>='0'&&c<='9')
    x=x*10+c-'0';
  return x*f;
}
IL void add(int x,int y){e[++num]=(kk){y,fir[x]},fir[x]=num;}
void dfs(int u){
	if(vis[u]){
		int x=u;
		do{
			bo[x]=1,c.push_back(x),x=to[x];
		}while(x^u);
		return;
	}
	vis[u]=1;dfs(to[u]);
}
void dele(int x){
	l[l[x].pre].nxt=l[x].nxt,
	l[l[x].nxt].pre=l[x].pre;
}
void dfs1(int u,int f,int id){
	dep[u]=dep[f]+1,vis[u]=1;
	if(dep[u]>d[id].size()) d[id].push_back(1);
	else if(dep[u]) ++d[id][dep[u]-1];
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt)
		if(!bo[v]) dfs1(v,u,id);
}
void dfs2(int u){
	siz[u]=1;
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt)
		if(!bo[v]){
			dfs2(v),siz[u]+=siz[v];
			if(siz[son[u]]<siz[v]) son[u]=v;
		}
}
void calc(int u,int rt){
	++buk[dep[u]];
	if(buk[dep[u]]>=k+1) ans[rt]=min(ans[rt],dep[u]-dep[rt]);
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt) calc(v,rt);
}
void clear(int u){
	buk[dep[u]]=0;
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt) clear(v);
}
void dfs3(int u,int op){
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt)
		if(!bo[v]&&v^son[u]) dfs3(v,0);
	if(son[u]) dfs3(son[u],1),ans[u]=min(ans[u],ans[son[u]]+1);
	++buk[dep[u]];
	for(int i=fir[u],v;v=e[i].to;i=e[i].nxt)
		if(!bo[v]&&v^son[u]) calc(v,u);
	if(!op){
		buk[dep[u]]=0;
		for(int i=fir[u],v;v=e[i].to;i=e[i].nxt) if(!bo[v]) clear(v);
	}
}
IL int mod(int x){return x>=p?x-p:x;}
void work(int st){
	c.clear();int Max=0;
	dfs(st);dep[0]=-1;p=c.size();
	for(int i=0;i<c.size();++i) val[i]=0,dfs1(c[i],0,c[i]),dfs2(c[i]),dfs3(c[i],0);
	for(int i=0;i<c.size();++i) Max=max(Max,(int)d[c[i]].size());
	for(int i=1;i<=p;++i) l[i]=(hh){i-1,i+1};l[0].nxt=1,l[p].nxt=0,l[0].pre=p;
	Max+=c.size()+2;
	for(int i=0;i<=Max;++i){
		int now=l[0].nxt;
		while(now){
			int u=c[now-1];
			if(d[u].size()>i){
				int pre=mod((now-i-1)%p+p);
				val[pre]+=d[u][i];
				if(val[pre]>=k) ans[u]=min(ans[u],i+1);
				now=l[now].nxt;
			}
			else dele(now),now=l[now].nxt;
		}
	}
	int Min=1e9,pos=0;
	for(int i=0;i<c.size();++i)
	  if(Min>ans[c[i]]) Min=ans[c[i]],pos=i;
	for(int i=mod(pos+1);i!=pos;i=mod(i+1)){
		++Min,Min=ans[c[i]]=min(ans[c[i]],Min);
	}
}
void solve(){
	n=in(),k=in();c.clear();
	for(int i=1;i<=n;++i) ans[i]=1e9,to[i]=in(),add(to[i],i);
	--k;
	for(int i=1;i<=n;++i) if(!vis[i]) work(i);
	for(int i=1;i<=n;++i)
	  if(ans[i]==1e9) printf("-1 ");
	  else printf("%d ",ans[i]);
}
int main()
{
	int T=1;
	while(T--) solve();
  return 0;
}

C Cmostp

题意:给定字符串 S S S q q q 次询问 S S S 的全部本质不同子串中有多少个子串的右端点落入 [ l , r ] [l,r] [l,r] 中的。 ∣ S ∣ , q ≤ 5 × 1 0 5 |S|,q \leq 5\times 10^5 S,q5×105

解法:显然在 SAM 上可以将前缀 [ 1 , i ] [1,i] [1,i] 对应节点在扩展的时候找到,记该节点为 d i d_i di。同时每个节点对应了 l l l 个本质不同子串。那么问题转化为, ∀ k ∈ [ l , r ] \forall k \in [l,r] k[l,r] d k d_k dk 到根节点的全部链的并上节点的 l l l 之和。这是由于 d k d_k dk 对应的这些串的 endpos 必然有元素 k k k,那么它所有 link 树上的父亲的 endpos 集合都必然有元素 k k k,因而都需要计入该次询问。

考虑根据 r r r 从小到大的离线询问,对 link 树进行树链剖分。依次插入第 i i i 个终止节点,首先使用树链剖分将 i i i 到根节点的链找到,并对链上的全部点染色成当前的 i i i(即进行 Access 操作),这样做的目的在于更新每个 link 树上节点 endpos 集合中小于等于 i i i 的最大值,而显然能更新的只有从 d i d_i di 节点沿 link 树上的全部祖先,因为只有这些节点的 endpos 集合中有 i i i

该步骤可以通过树链剖分+区间合并线段树完成。对于每个线段树上节点,维护它所对应链的左侧颜色和右侧颜色,以及最靠右(对应于树上的更深侧)的颜色变化点 div(即从这里开始颜色发生变化),那么这个信息非常容易区间合并。同时同步的使用树状数组维护前面的 j j j 条链现在的长度 w j w_j wj(即颜色为 j j j 的点的 l l l 之和),那么对于固定 r r r 的全部询问 [ l , r ] [l,r] [l,r],答案就是 ∑ k = l r w k \displaystyle \sum_{k=l}^r w_k k=lrwk

因而总的时间复杂度为 O ( n log ⁡ 2 n ) \mathcal O(n\log^2n) O(nlog2n)

#include 
using namespace std;
class BIT
{
    vector<int> t;
    int n;
    int lowbit(int x)
    {
        return x & (-x);
    }
    long long query(int x)
    {
        long long ans = 0;
        while (x)
        {
            ans += t[x];
            x -= lowbit(x);
        }
        return ans;
    }

public:
    void set(int n)
    {
        this->n = n;
        t.resize(n + 1);
    }
    void update(int x, int k)
    {
        while (x <= n)
        {
            t[x] += k;
            x += lowbit(x);
        }
    }
    long long query(int l, int r)
    {
        return query(r) - query(l - 1);
    }
};
struct node
{
    int leftcol, rightcol;
    int left, right;
    int div;
    int tag;
    node()
    {
        left = right = leftcol = rightcol = div = tag = 0;
    }
    node(int _left, int _right, int _leftcol, int _rightcol)
    {
        left = _left;
        right = _right;
        leftcol = _leftcol;
        rightcol = _rightcol;
        div = tag = 0;
    }
    node operator+(const node b) const
    {
        node ans(left, b.right, leftcol, b.rightcol);
        if (b.div)
            ans.div = b.div;
        else if (rightcol != b.leftcol)
            ans.div = right;
        else if (div)
            ans.div = div;
        return ans;
    }
};
struct query
{
    int id;
    int l, r;
    query(int _id, int _l, int _r)
    {
        l = _l;
        r = _r;
        id = _id;
    }
    bool operator<(const query &b) const
    {
        return r < b.r;
    }
};
class segment_tree
{

    vector<node> t;
    int n;
    void pushdown(int place, int left, int right)
    {
        if (t[place].tag)
        {
            int mid = (left + right) >> 1;
            update(place << 1, left, mid, left, mid, t[place].tag);
            update(place << 1 | 1, mid + 1, right, mid + 1, right, t[place].tag);
            t[place].tag = 0;
        }
    }
    void update(int place, int left, int right, int start, int end, int x)
    {
        if (start <= left && right <= end)
        {
            t[place].div = 0;
            t[place].leftcol = t[place].rightcol = x;
            t[place].tag = x;
            return;
        }
        pushdown(place, left, right);
        int mid = (left + right) >> 1;
        if (start <= mid)
            update(place << 1, left, mid, start, end, x);
        if (end > mid)
            update(place << 1 | 1, mid + 1, right, start, end, x);
        t[place] = t[place << 1] + t[place << 1 | 1];
    }
    void build(int place, int left, int right)
    {
        t[place] = node(left, right, 0, 0);
        if (left == right)
            return;
        int mid = (left + right) >> 1;
        build(place << 1, left, mid);
        build(place << 1 | 1, mid + 1, right);
    }
    node query(int place, int left, int right, int start, int end)
    {
        if (start <= left && right <= end)
            return t[place];
        pushdown(place, left, right);
        int mid = (left + right) >> 1;
        if (end <= mid)
            return query(place << 1, left, mid, start, end);
        if (start > mid)
            return query(place << 1 | 1, mid + 1, right, start, end);
        return query(place << 1, left, mid, start, end) + query(place << 1 | 1, mid + 1, right, start, end);
    }

public:
    void set(int n)
    {
        this->t.resize(4 * n + 5);
        this->n = n;
        build(1, 1, n);
    }
    void update(int l, int r, int col)
    {
        update(1, 1, n, l, r, col);
    }
    node query(int l, int r)
    {
        // printf("L:%d R:%d DIV:%d\n", l, r, query(1, 1, n, l, r).div);
        return query(1, 1, n, l, r);
    }
};
class Tree_Split
{
    vector<vector<int>> graph;
    BIT num_edge;
    segment_tree t;
    vector<int> maxson, siz, father, tp, id, val, revid;
    int ind;
    void dfs1(int place, int fa)
    {
        father[place] = fa;
        siz[place] = 1;
        for (auto i : graph[place])
            if (i != fa)
            {
                dfs1(i, place);
                siz[place] += siz[i];
                if (!maxson[place] || siz[maxson[place]] < siz[i])
                    maxson[place] = i;
            }
    }
    void dfs2(int place, int ancestor)
    {
        id[place] = ++ind;
        revid[ind] = place;
        tp[place] = ancestor;
        if (maxson[place])
            dfs2(maxson[place], ancestor);
        for (auto i : graph[place])
            if (i != father[place] && i != maxson[place])
                dfs2(i, i);
    }

public:
    Tree_Split(vector<vector<int>> &graph, vector<int> &status_len, int m)
    {
        int n = graph.size() - 1;
        ind = 0;
        num_edge.set(m);
        this->graph = graph;
        val = status_len;
        tp.resize(n + 1);
        id.resize(n + 1);
        revid.resize(n + 1);
        maxson.resize(n + 1);
        siz.resize(n + 1);
        father.resize(n + 1);
        dfs1(1, 0);
        dfs2(1, 1);
        t.set(n);
    }
    void access(int x, int col)
    {
        if (col == 1)
        {
            num_edge.update(1, val[x]);
            while (x)
            {
                t.update(id[tp[x]], id[x], col);
                x = father[tp[x]];
            }
        }
        else
        {
            while(x)
            {
                int ori = x;
                while(1)
                {
                    node temp = t.query(id[tp[x]], id[ori]);
                    int div = temp.div;
                    if(!div)
                        div = tp[x];
                    else
                        div = revid[div + 1];
                    if(temp.rightcol)
                        num_edge.update(temp.rightcol, val[father[div]] - val[ori]);
                    num_edge.update(col, val[ori] - val[father[div]]);
                    if (div == tp[x])
                        break;
                    ori = father[div];
                }
                t.update(id[tp[x]], id[x], col);
                x = father[tp[x]];

            }
        }
    }
    long long query(int l, int r)
    {
        return num_edge.query(l, r);
    }
};
class SAM
{
    const int shift = 97;
    struct node
    {
        int ch[26];
        int len;
        int father;
        node()
        {
            memset(ch, 0, sizeof(ch));
            len = father = 0;
        }
    } NIL;
    vector<node> t;
    int last, ind;
    int insert(int c)
    {
        int p = last;
        int np = last = ++ind;
        t.push_back(NIL);
        t[np].len = t[p].len + 1;
        for (; p && !t[p].ch[c]; p = t[p].father)
            t[p].ch[c] = np;
        if (!p)
            t[np].father = 1;
        else
        {
            int q = t[p].ch[c];
            if (t[p].len + 1 == t[q].len)
                t[np].father = q;
            else
            {
                int nq = ++ind;
                t.push_back(t[q]);
                t[nq].len = t[p].len + 1;
                t[q].father = t[np].father = nq;
                for (; p && t[p].ch[c] == q; p = t[p].father)
                    t[p].ch[c] = nq;
            }
        }
        return last;
    }
    vector<int> pos;
    vector<vector<int>> graph;

public:
    SAM(string s)
    {
        last = ind = 1;
        t.push_back(NIL);
        t.push_back(NIL);
        for (auto i : s)
            pos.push_back(insert(i - shift));
        graph.resize(t.size());
        for (int i = 2; i <= ind; i++)
            graph[t[i].father].push_back(i);
    }
    vector<long long> solve(vector<query> &que)
    {
        vector<int> len(t.size());
        for (int i = 1; i <= ind;i++)
            len[i] = t[i].len;
        int n = pos.size(), q = que.size(), place = 0;
        sort(que.begin(), que.end());
        vector<long long> ans(q);
        Tree_Split tr(graph, len, n);
        for (int i = 0; i < n; i++)
        {
            tr.access(pos[i], i + 1);
            while (place < q && que[place].r == i + 1)
            {
                ans[que[place].id] = tr.query(que[place].l, que[place].r);
                place++;
            }
        }
        return ans;
    }
};
int main()
{
    cin.tie(0)->sync_with_stdio(0);
    cin.exceptions(cin.failbit);
    cin.tie(NULL);
    cout.tie(NULL);
    string s;
    int n, q;
    cin >> n >> q >> s;
    SAM solve(s);
    vector<query> que;
    for (int i = 0, l, r; i < q;i++)
    {
        cin >> l >> r;
        que.emplace_back(i, l, r);
    }
    for (auto i : solve.solve(que))
        cout << i << "\n";
    return 0;
}

E Everyone is bot

题意: n n n 个人打算复读。一次复读的过程如下,每一轮, n n n 个人按照编号从小到大依次执行以下操作:如果这个人在前几轮已经进行过复读,他不会再次复读,否则他可以选择复读。如果某一轮没有人进行复读,那么复读的过程结束。 结束后选择复读的倒数第 p p p 个人会扣分,而其他的人会得分(第 i i i 个人若为第 j j j 个复读的,则会获得 a i , j > 0 a_{i,j}>0 ai,j>0 的分数)。每个人最大化得分,问最终每个人的分数。 1 ≤ p ≤ n ≤ 1 × 1 0 3 1 \leq p \leq n \leq 1\times 10^3 1pn1×103

解法:只有 n   m o d   p n \bmod p nmodp 个人会得分。

若最后只有 p p p 个人还没复读,剩下的人都已经复读了,那么最后的 p p p 个人谁都不会说话:若某个人选择复读,则后面每个人都因为不会被扣分而选择复读,那么谁开启了最后 p p p 个人的复读谁就会扣分,而让别人加分,因而没人会当冤大头。

那么对于 n − 2 p + 1 n-2p+1 n2p+1 到第 n − p n-p np 个选择复读的人,他们同样会陷入这个困境:因为一旦最后的 p p p 个人因为上面的逻辑没人去复读为第一个开启这一段复读的人埋单(使得他不会成为倒数第 p p p 个),那么这一段也会因为同样的逻辑每个人都选择不复读,以保证自己不会被扣分。同理最后的每 p p p 个人都会因为同样的逻辑都不复读,哪怕 a i , j a_{i,j} ai,j 非常的大。因而最后只有 n   m o d   p n \bmod p nmodp 个人会复读。这些人因为是一定会复读的(没有人可以使得他们成为倒数第 p p p 个),所以排在前面的一定会先下手为强。所以前 n   m o d   p + 1 n \bmod p+1 nmodp+1 个人按顺序复读。

#include 
using namespace std;
const int N = 1000;
int a[N + 5][N + 5];
int main()
{
    int n, p;
    scanf("%d%d", &n, &p);
    for (int i = 1; i <= n;i++)
        for (int j = 1; j <= n;j++)
            scanf("%d", &a[i][j]);
    for (int i = 1; i <= n % p; i++)
        printf("%d ", a[i][i]);
    for (int i = 1; i <= n - n % p; i++)
        printf("0 ");
    return 0;
}

G Good red-string

题意:给定一个长度为 n n n 的串,仅由 red?构成。问能够找到一种将全部的 ?转化为 red中的一种字符,使得最后的串由 n 3 \dfrac{n}{3} 3n 个不相交的 red子序列构成。例如 reredd符合条件而 rederd不符合条件。保证 n n n 为三的倍数, n ≤ 3 × 1 0 5 n \leq 3\times 10^5 n3×105

解法:考虑对 e的约束条件:每一个 d前必须至少有一个 e;每一个 r后面至少有一个 e。因而仅针对这两条关系,可以用栈去安排出刚需的问号转化方法。对于剩下的问号,则从左到右的尽可能安排 red即可。最后检查一下是否符合条件即可。

这样做的正确性在于,所有的约束关系都集中到 e这里,也就是在单独对 r贪心的时候,也是符合 d的贪心要求的。

#include 
using namespace std;
bool solve(string &str)
{
    int r = 0, e = 0, d = 0, n = str.length();
    stack<int> s;
    for (int i = 0; i < n; i++)
    {
        if (str[i] == 'e')
            e++;
        else if (str[i] == 'd')
        {
            if (e)
                e--;
            else if (!s.empty())
            {
                str[s.top()] = 'e';
                s.pop();
            }
            else
                return false;
        }
        else if (str[i] == '?')
            s.push(i);
    }
    while (!s.empty())
        s.pop();
    e = 0;
    for (int i = n - 1; i >= 0; i--)
    {
        if (str[i] == 'e')
            e++;
        else if (str[i] == 'r')
        {
            if (e)
                e--;
            else if (!s.empty())
            {
                str[s.top()] = 'e';
                s.pop();
            }
            else
                return false;
        }
        else if (str[i] == '?')
            s.push(i);
    }
    e = 0;
    for (auto i : str)
        if (i == 'r')
            r++;
        else if (i == 'e')
            e++;
        else if (i == 'd')
            d++;
    for (int i = 0; i < n; i++)
        if (str[i] == '?')
        {
            if (3 * r < n)
            {
                str[i] = 'r';
                r++;
            }
            else if (3 * e < n)
            {
                str[i] = 'e';
                e++;
            }
            else if (3 * d < n)
            {
                str[i] = 'd';
                d++;
            }
        }
    r = e = d = 0;
    for (auto i : str)
    {
        if (i == 'r')
            r++;
        if (i == 'e')
            e++;
        if (i == 'd')
            d++;
        if (r < e || e < d)
            return false;
    }
    if (r != e || e != d)
        return false;
    else
        return true;
}
int main()
{
    string s;
    int t;
    scanf("%d", &t);
    while (t--)
    {
        cin >> s;
        if (solve(s))
            printf("Yes\n");
        else
            printf("No\n");
    }
    return 0;
}

H Here is an Easy Problem of Zero-chan

题意:给定一个 n n n 个节点以 1 1 1 为根的树, q q q 次询问,每次给定 x x x,问 ∏ i = 1 n l c a ( x , i ) \displaystyle \prod_{i=1}^n {\rm lca}(x,i) i=1nlca(x,i) 的后缀有多少个 0 0 0 n , q ≤ 1 × 1 0 5 n,q \leq 1\times 10^5 n,q1×105

解法:可以通过换根计算出每个点的后缀 0 0 0 个数。显然 x = 1 x=1 x=1 时答案为 0 0 0。考虑从 u u u 节点走到它的一个儿子 v v v,那么 l c a ( x , i ) {\rm lca}(x,i) lca(x,i) v v v 出现次数增加 s i z e v {\rm size}_v sizev 次,而 u u u 的出现次数减少 s i z e v {\rm size}_v sizev 次。因而可以通过维护 2 2 2 5 5 5 因子个数 O ( 1 ) O(1) O(1) 的统计答案。

#include 
using namespace std;
const int N = 100000;
struct line
{
    int from;
    int to;
    int next;
};
struct line que[2 * N + 5];
int cnt, headers[N + 5], siz[N + 5];
void add(int from, int to)
{
    cnt++;
    que[cnt].from = from;
    que[cnt].to = to;
    que[cnt].next = headers[from];
    headers[from] = cnt;
}
int ans[N + 5];
int cnt2[N + 5], cnt5[N + 5];
void dfs1(int place, int father)
{
    siz[place] = 1;
    for (int i = headers[place]; i; i = que[i].next)
        if (que[i].to != father)
        {
            dfs1(que[i].to, place);
            siz[place] += siz[que[i].to];
        }
}
void dfs2(int place, int father, int two, int five)
{
    ans[place] = min(two, five);
    for (int i = headers[place]; i; i = que[i].next)
        if(que[i].to != father)
            dfs2(que[i].to, place, two - siz[que[i].to] * (cnt2[place] - cnt2[que[i].to]), five - siz[que[i].to] * (cnt5[place] - cnt5[que[i].to]));
}
int main()
{
    for (int i = 1; i <= N;i++)
    {
        int x = i;
        while (x % 2 == 0)
        {
            x >>= 1;
            cnt2[i]++;
        }
        x = i;
        while (x % 5 == 0)
        {
            x /= 5;
            cnt5[i]++;
        }
    }
    int n, q;
    scanf("%d%d", &n, &q);
    for (int i = 1, u, v; i < n;i++)
    {
        scanf("%d%d", &u, &v);
        add(u, v);
        add(v, u);
    }
    dfs1(1, 1);
    dfs2(1, 1, 0, 0);
    for (int i = 1, x; i <= q;i++)
    {
        scanf("%d", &x);
        printf("%d\n", ans[x]);
    }
    return 0;
}

J Jellyfish and its dream

题意:给定长度为 n n n 的环形序列 a 0 , a 1 , ⋯   , a n − 1 a_0,a_1,\cdots,a_{n-1} a0,a1,,an1,仅由 0 , 1 , 2 0,1,2 0,1,2 构成。若 a i + 1 ≡ a i   m o d   n + 1 ( m o d 3 ) a_i+1 \equiv a_{i \bmod n+1} \pmod 3 ai+1aimodn+1(mod3),则令 a i ← ( a i + 1 )   m o d   3 a_i \leftarrow (a_i+1) \bmod 3 ai(ai+1)mod3。问能否经过若干次操作使得 a 0 = a 1 = ⋯ = a n − 1 a_0=a_1=\cdots=a_{n-1} a0=a1==an1 n ≤ 2 × 1 0 5 n \leq 2\times 10^5 n2×105

解法:相邻加一考虑差分。设 b b b 数组为 a a a 的差分数组,最终的目标是需要让 b i = 0 b_i=0 bi=0。则对于 b b b 数组中出现了 ( 2 , 1 ) (2,1) (2,1) 的情况( b i = 2 , b i + 1 = 1 b_i=2,b_{i+1}=1 bi=2,bi+1=1),则必然可以经过该操作使得 b i = b i + 1 = 0 b_i=b_{i+1}=0 bi=bi+1=0 ( 1 , 1 ) (1,1) (1,1) 变成 ( 2 , 0 ) (2,0) (2,0),而 ( 0 , 1 ) (0,1) (0,1) 变成 ( 1 , 0 ) (1,0) (1,0)。而通过 ( 0 , 1 ) → ( 1 , 0 ) (0,1) \to (1,0) (0,1)(1,0) 可以将 1 1 1 平移,最终为了要消掉所有的 2 2 2,只能通过 ( 2 , 1 ) → ( 0 , 0 ) (2,1) \to (0,0) (2,1)(0,0) 来完成。因而 2 2 2 的数目不可以多于 1 1 1 的数目。

#include 
using namespace std;
int main()
{
    int t, n;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d", &n);
        vector<int> a(n);
        for (int i = 0; i < n;i++)
            scanf("%d", &a[i]);
        int cnt = 0;
        for (int i = 0; i < n;i++)
            if (a[i] < a[(i + 1) % n])
                cnt++;
            else if (a[i] > a[(i + 1) % n])
                cnt--;
        if (cnt < 0)
            printf("No\n");
        else
            printf("Yes\n");
    }
    return 0;
}

K Killer Sajin’s Matrix

题意:给定 n × m n\times m n×m 的矩阵,要求在其中填入 k k k 1 1 1 使得每行每列 1 1 1 的个数均为奇数。输出一个合法方案或者报告无解。 n , m , k ≤ 1 × 1 0 5 n,m,k \leq 1\times 10^5 n,m,k1×105

解法:(感谢 Oscar 提供的做法)

显然通过不断的交换列与行可以使得所有的 1 1 1 都安放在斜对角的矩阵中,且每行每列必然至少有一个 1 1 1,如下图的形态:

2022 年牛客多校加赛场补题记录_第1张图片
设每个满 1 1 1 矩阵的长和宽分别为 x i , y i x_i,y_i xi,yi,则有 ∑ x i = n \sum x_i=n xi=n ∑ y i = m \sum y_i=m yi=m,同时 ∑ x i y i = k \sum x_iy_i=k xiyi=k。注意到 x i , y i x_i,y_i xi,yi 必然为奇数,则矩阵个数与 n , m , k n,m,k n,m,k 奇偶性相同,所以 n , m , k n,m,k n,m,k 的奇偶性必须完全相同。

考虑 n , m , k n,m,k n,m,k 均为奇数的情况。首先考虑一个 3 × 3 3\times 3 3×3 的基本型:

2022 年牛客多校加赛场补题记录_第2张图片

对于 3 × 3 3\times 3 3×3 的矩形,最少需要斜对角的 3 3 3 个保证每行每列都有一个 1 1 1。可以通过删除中间的一个,增补周围的 3 3 3 个变成一个 L 型,使用 5 5 5 1 1 1。通过这一变换使得 1 1 1 的个数增加了 2 2 2

显然最少需要 max ⁡ ( n , m ) \max(n,m) max(n,m) 1 1 1,此时的放置方案为斜对角的放置 min ⁡ ( n , m ) \min(n,m) min(n,m) 1 1 1,再在最后一行放置 max ⁡ ( n , m ) − min ⁡ ( n , m ) \max(n,m)-\min(n,m) max(n,m)min(n,m) 1 1 1,如下所示:

2022 年牛客多校加赛场补题记录_第3张图片

考虑当 1 1 1 增多的时候如何修改。显然当 k ≤ n + m − 1 k \leq n+m-1 kn+m1 时,我们一直可以将一个 3 × 3 3\times 3 3×3 的斜对角通过删除中间一个 1 1 1,增加周围的 3 3 3 1 1 1 来增加两个 1 1 1:(该步操作可以称为 3 → 5 3\to 5 35
2022 年牛客多校加赛场补题记录_第4张图片

那么当我们达成了 n + m − 1 n+m-1 n+m1:也就是周围第一列、最后一行全都是 1 1 1 之后,对于内侧的 ( n − 1 ) × ( m − 1 ) (n-1)\times (m-1) (n1)×(m1),因为 n − 1 , m − 1 n-1,m-1 n1,m1 均为偶数,因而可以一口气放上一个 2 × 2 2\times 2 2×2 而不影响任何的奇偶性:

2022 年牛客多校加赛场补题记录_第5张图片

但是这样的操作不会改变 1 1 1 的个数对 4 4 4 的余数。因而如果 k − ( n + m − 1 ) ≡ 2 ( m o d 4 ) k-(n+m-1) \equiv 2\pmod 4 k(n+m1)2(mod4),则最后一步 3 → 5 3\to 5 35 操作不应当进行,以调整最后的余数。

2022 年牛客多校加赛场补题记录_第6张图片

注意到 k = ( n − 1 ) ( m − 1 ) − 2 k=(n-1)(m-1)-2 k=(n1)(m1)2 是必然无解的:最后仅剩的两格必然会让所在的行或列不满足要求。

接下来考虑偶数的方案。同样基于该思想,首先构造一个 k k k 最小的答案:

2022 年牛客多校加赛场补题记录_第7张图片

然后以两行为基本单元开始不断调整(省略剩下的 4 4 4 行),其基本原理还是逐步增加 2 2 2 个格子:

2022 年牛客多校加赛场补题记录_第8张图片

最大的 k k k n m − max ⁡ ( n , m ) nm-\max(n,m) nmmax(n,m):即反选的答案。

可以参考 Oscar 的代码:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=53401391

L Lndjy and the mex

题意:给定长度为 n n n 的序列 { a i } \{a_i\} {ai},满足 ∑ a i = n \sum a_i=n ai=n。问由 a 0 a_0 a0 0 0 0 a 1 a_1 a1 1 1 1,……, a n a_n an n n n 构成的全部 n ! ∏ i = 0 n a i ! \displaystyle \dfrac{n!}{\prod_{i=0}^n a_i!} i=0nai!n! 个序列中,每个序列的全部连续子序列的 m e x \rm mex mex 值之和。 n ≤ 1 × 1 0 5 n \leq 1\times 10^5 n1×105

解法:显然直接求 m e x \rm mex mex 是非常不好做的,尝试将其转化为方案数。设 f i f_i fi 表示连续子序列的 m e x \rm mex mex 值为 i i i 的连续子序列个数,则答案为 ∑ i = 0 + ∞ i f i \displaystyle \sum_{i=0}^{+\infty}if_i i=0+ifi,令 g i = ∑ j = i + ∞ f j \displaystyle g_i=\sum_{j=i}^{+\infty}f_j gi=j=i+fj,则有:
∑ i = 0 + ∞ i f i = ∑ i = 1 + ∞ i ( g i − g i + 1 ) = ∑ i = 1 + ∞ g i \begin{aligned} \sum_{i=0}^{+\infty}if_i=&\sum_{i=1}^{+\infty}i(g_{i}-g_{i+1})\\ =&\sum_{i=1}^{+\infty}g_i \end{aligned} i=0+ifi==i=1+i(gigi+1)i=1+gi
以上的化简是用到了 g + ∞ = 0 g_{+\infty}=0 g+=0

注意到 g i g_i gi 的组合意义为,连续子序列 m e x \rm mex mex 值大于等于 i i i 的序列个数,等价于 0 ∼ i − 1 0 \sim i-1 0i1 的数字至少出现一次,其余数字随意的方案数。考虑到每个数字选了多少个,假设第 i i i 个数字选了 b i b_i bi 个,那么有:
g i = ∑ b 0 = 1 a 0 ∑ b 1 = 1 a 1 ⋯ ∑ b i − 1 = 1 a i − 1 ∑ b i = 0 a i ∑ b i + 1 = 0 a i + 1 ⋯ ∑ b n = 0 a n ( ∑ j = 0 n b j b 0 , b 1 , ⋯   , b n ) ( n − ∑ j = 0 n b j a 0 − b 0 , a 1 − b 1 , ⋯   , a n − b n ) ( n − ∑ j = 0 n b j + 1 ) g_i=\sum_{b_0=1}^{a_0}\sum_{b_1=1}^{a_1}\cdots \sum_{b_{i-1}=1}^{a_{i-1}}\sum_{b_i=0}^{a_i}\sum_{b_{i+1}=0}^{a_{i+1}}\cdots \sum_{b_{n}=0}^{a_{n}} {\sum_{j=0}^n b_j \choose b_0,b_1,\cdots,b_n}{n-\sum_{j=0}^n b_j \choose a_0-b_0,a_1-b_1,\cdots,a_n-b_n}\left(n-\sum_{j=0}^nb_j+1\right) gi=b0=1a0b1=1a1bi1=1ai1bi=0aibi+1=0ai+1bn=0an(b0,b1,,bnj=0nbj)(a0b0,a1b1,,anbnnj=0nbj)(nj=0nbj+1)
即首先是连续子序列内部的排列,再是外部的排列,最后再将这个连续子序列插入完整的序列。那么这个问题就可以非常容易的使用 EGF 得到解决:设 f i ( x ) f_i(x) fi(x) 表示数字 i i i 的选择方案的 EGF, f i ( x ) = ∑ j = 0 a i x j j ! ( a i − j ) ! \displaystyle f_i(x)=\sum_{j=0}^{a_i}\dfrac{x^j}{j!(a_i-j)!} fi(x)=j=0aij!(aij)!xj g i ( x ) = f i ( x ) − 1 a i ! g_i(x)=f_i(x)-\dfrac{1}{a_i!} gi(x)=fi(x)ai!1 表示至少选一个的方案,则最终的答案为:
∑ s = 0 n ( n − s + 1 ) ! ( s ! [ x s ] ∑ i = 0 n − 1 ∏ j = 0 i g j ( x ) ∏ j = i + 1 n f j ( x ) ) \sum_{s=0}^n(n-s+1)!\left(s![x^s]\sum_{i=0}^{n-1}\prod_{j=0}^{i}g_j(x)\prod_{j=i+1}^nf_j(x)\right) s=0n(ns+1)!(s![xs]i=0n1j=0igj(x)j=i+1nfj(x))
注意:此处和通常的 EGF 稍有区别。正常的 EGF 由于不考虑外侧的排列,为 f i ( x ) = ∑ j = 0 a i x j j ! \displaystyle f_i(x)=\sum_{j=0}^{a_i}\dfrac{x^j}{j!} fi(x)=j=0aij!xj。而当选定连续子序列中数字个数确定,外侧也随之确定,他们的排列数为 ( n − s + 1 ) ! ( a i − j ) ! \dfrac{(n-s+1)!}{(a_i-j)!} (aij)!(ns+1)!,因而可以将此系数杂糅进 f i ( x ) f_i(x) fi(x)

考虑使用分治 NTT 去计算上式:对于区间 ( l , r ) (l,r) (l,r) 维护 F 1 ( x , l , r ) = ∏ i = l r g i ( x ) , F 2 ( x , l , r ) = ∏ i = l r f i ( x ) , F 3 ( x , l , r ) = ∑ i = l r − 1 ∏ j = l i g i ( x ) ∏ j = i + 1 r f i ( x ) \displaystyle F_1(x,l,r)=\prod_{i=l}^rg_i(x),F_2(x,l,r)=\prod_{i=l}^rf_i(x),F_3(x,l,r)=\sum_{i=l}^{r-1}\prod_{j=l}^{i}g_i(x)\prod_{j=i+1}^rf_i(x) F1(x,l,r)=i=lrgi(x),F2(x,l,r)=i=lrfi(x),F3(x,l,r)=i=lr1j=ligi(x)j=i+1rfi(x),则有 F 3 ( x , l , r ) = F 1 ( x , l , m ) F 3 ( x , m + 1 , r ) + F 3 ( x , l , m ) F 2 ( x , m + 1 , r ) − F 1 ( x , l , m ) F 2 ( x , m + 1 , r ) F_3(x,l,r)=F_1(x,l,m)F_3(x,m+1,r)+F_3(x,l,m)F_2(x,m+1,r)-F_1(x,l,m)F_2(x,m+1,r) F3(x,l,r)=F1(x,l,m)F3(x,m+1,r)+F3(x,l,m)F2(x,m+1,r)F1(x,l,m)F2(x,m+1,r)。因而可以 O ( n log ⁡ 2 n ) \mathcal O(n\log^2n) O(nlog2n) 的通过。

Poly f[N + 5], g[N + 5], gf[N + 5];
int a[N + 5];

void build(int place, int left, int right)
{
    if (left == right)
    {
        f[place].resize(a[left] + 1);
        for (int i = 0; i <= a[left]; i++)
            f[place][i] = 1ll * ifac[i] * ifac[a[left] - i] % P;
        g[place] = f[place];
        g[place][0] = 0;
        gf[place] = f[place] + g[place];
        return;
    }
    int mid = (left + right) >> 1;
    build(place << 1, left, mid);
    build(place << 1 | 1, mid + 1, right);
    f[place] = f[place << 1] * f[place << 1 | 1];
    g[place] = g[place << 1] * g[place << 1 | 1];
    gf[place] = g[place << 1] * gf[place << 1 | 1] + gf[place << 1] * f[place << 1 | 1] - g[place << 1] * f[place << 1 | 1];
}
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i <= n;i++)
        scanf("%d", &a[i]);
    build(1, 0, n);
    Poly F = gf[1] - f[1];
    int ans = 0;
    for (int i = 1; i <= n; i++)
        ans = (ans + 1ll * (n - i + 1) * fac[i] % P * fac[n - i] % P * F[i]) % P;
    printf("%d", ans);
    return 0;
}

你可能感兴趣的:(补题记录,链表,算法,数据结构)