省流:\(100+50+10+30\)。还是不稳定啊,noip上不了270就真的要退役了。
T1
题意:给定一个长度为 \(n\) 的序列 \(a\),每次你可以交换相邻两个位置,求出最小交换次数以及字典序最小的交换方案使得 \(a\) 的每个不是本身的前缀都不是排列。
\(n \leq 10^5\)。
注意到每次交换至多会减少一个是排列的前缀。因此最少操作次数就是这样的前缀个数。
至于最小字典序,直接从前往后操作即可。
时间复杂度 \(\Theta(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,a[N];
vector<int> ve;
int main() {ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr);cin>>n;for(int i=1; i<=n; i++) cin>>a[i];int mx=0,mn=n+1;for(int i=1; i<n; i++) {if(min(mn,a[i])==1&&max(mx,a[i])==i) ve.push_back(i),swap(a[i],a[i+1]);mn=min(mn,a[i]),mx=max(mx,a[i]);}cout<<ve.size()<<'\n';for(int i=0; i<ve.size(); i++) cout<<ve[i]<<" ";return 0;
}
T2
题意:给定一个 \(n \times m\) 的网格,你需要操作一个角色走出网格。每个格子上有一个给定的方向,当角色站在这个格子上时,可以向格子给定的方向走不超过 \(k\) 步,求有多少个格子可以作为出发点让角色走出网格。
\(k \leq n,m \leq 3000\)。
考虑倒着做,如果一个格子可以走出网格,那么其余能够通过走一步到达这个格子的格子也能走出。于是我们可以对于每个格子与它上面第一个 D,下面第一个 U,左边第一个 R,右边第一个 L 连边,前提是距离这个格子不超过 \(k\),然后把可以一步走出网格的格子扔进队列跑多源 bfs 即可。容易证明这样一定能遍历到所有能够走出网格的格子。
时间复杂度 \(\Theta(nm)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=3005;
char ch[N][N];
int n,m,k,vis[N][N],head[N][N],ecnt=0;
struct edge {int tox,toy,nxt;}e[N*N<<2];
inline void add(int x,int y,int xx,int yy) {e[++ecnt]=(edge){xx,yy,head[x][y]};head[x][y]=ecnt;}
inline bool check(int x,int y) {if(ch[x][y]=='D') if(x+k>n) return true;if(ch[x][y]=='U') if(x-k<1) return true;if(ch[x][y]=='L') if(y-k<1) return true;if(ch[x][y]=='R') if(y+k>m) return true;return false;
}
int main() {ios::sync_with_stdio(false);cin.tie(nullptr),cout.tie(nullptr);cin>>n>>m>>k;for(int i=1; i<=n; i++) for(int j=1; j<=m; j++) cin>>ch[i][j];for(int i=1; i<=n; i++) {int lst=0;for(int j=1; j<=m; j++) {if(lst&&j-lst<=k) add(i,j,i,lst);if(ch[i][j]=='R') lst=j;}}for(int i=1; i<=n; i++) {int lst=0;for(int j=m; j>=1; j--) {if(lst&&lst-j<=k) add(i,j,i,lst);if(ch[i][j]=='L') lst=j;}}for(int i=1; i<=m; i++) {int lst=0;for(int j=1; j<=n; j++) {if(lst&&j-lst<=k) add(j,i,lst,i);if(ch[j][i]=='D') lst=j;}}for(int i=1; i<=m; i++) {int lst=0;for(int j=n; j>=1; j--) {if(lst&&lst-j<=k) add(j,i,lst,i);if(ch[j][i]=='U') lst=j;}}queue<pair<int,int>> q;for(int i=1; i<=n; i++) for(int j=1; j<=m; j++) if(check(i,j)) q.push(make_pair(i,j)),vis[i][j]=true;while(!q.empty()) {int x=q.front().first,y=q.front().second;q.pop();for(int i=head[x][y]; i; i=e[i].nxt) {int vx=e[i].tox,vy=e[i].toy;if(vis[vx][vy]) continue;vis[vx][vy]=true;q.push(make_pair(vx,vy));}}int ans=0;for(int i=1; i<=n; i++) for(int j=1; j<=m; j++) ans+=vis[i][j];cout<<ans;return 0;
}
T3
题意:你需要求出满足以下条件的长度为 \(n + 2\) 的 \(a\) 序列的数量。
- \(a_0 = a_{n + 1} = 0\)
- \(\forall i \in [1,n],a_i \geq 0\)
- \(\sum_{i = 0}^n \max(a_{i + 1} - a_i,0) = m\)
\(1 \leq n,m \leq 2 \times 10^5\)。
解法一:
注意到答案相当于:对每个长度为 \(2m\) 的合法括号序列,将其分为 \(n + 1\) 段(每段可以为空),要求每段内不能同时含有左右括号,求方案数之和。
发现一个括号序列的划分方案数只和其中 ()
子串的个数有关,对于左括号相当于从 \((x,y)\) 走到 \((x,y+1)\),右括号相当于 \((x,y)\) 走到 \((x+1,y)\),画出格路,就是只和拐弯次数有关(注意这个拐弯是先向上再向右的拐弯)。不妨称 ()
子串在格路中对应一个“拐点”,则要对每个 \(k\) 求从 \((0,0)\) 到 \((m,m)\),恰好有 \(k\) 个拐点,且始终有 \(x \leq y\) 的格路个数。为什么是对的呢?因为我们知道了拐点数为 \(2k - 1\),这些拐点是已经钦定的 \(a_i\),所以还剩下 \(n - 2k + 1\) 个点没有钦定,因为是从 \((0,0)\) 到 \((m,m)\),所以这些没钦定的点有 \(2m + 1\) 种选择,由于可重且点是相同的,所以钦定未钦定的点的方案是 \(C_{2m + n - 2k + 1}^{n - 2k + 1}\) 的。这是一个固定的值。
先考虑如果没有 \(x \leq y\) 的限制怎么做。只要在两个坐标上分别选 \(k\) 个拐点的位置,就唯一对应一条格路。显然方案数是 \((C_n^k)^2\)。这启发我们对“拐点坐标”构成的序列进行计数,而非对格路进行计数。
设拐点坐标为 \((x_1,y_1),(x_2,y_2),\cdots,(x_k,y_k)\)。于是我们要减去存在某个拐点满足 \(y_i < x_{i + 1}\) 的格路数,表示在第 \(i\) 个位置走到第 \(i + 1\) 个位置后不合法了。不妨假设 \(i\) 是第一个 \(y_i < x_{i + 1}\) 的拐点。那么这样的路径满足的充要条件是:
且
由于是格点,所以是小于。两维独立,所以对上述两个柿子分别计算方案数就行,这样的两个序列包含了所有 \(i\) 的情况。容易证明。
所以有 \(k\) 个拐点的方案数为:
对于每个 \(k\) 求和即可。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=7e5+5,p=1e9+7;
int n,m,ans=0,fac[N],inv[N];
int qpow(int a,int b) {int ans=1;while(b) {if(b&1) ans=ans*a%p;a=a*a%p;b>>=1;}return ans;
}
void init(int lim) {fac[0]=inv[0]=1;for(int i=1; i<=lim; i++) fac[i]=fac[i-1]*i%p;inv[lim]=qpow(fac[lim],p-2);for(int i=lim-1; i>=1; i--) inv[i]=inv[i+1]*(i+1)%p;
}
int C(int n,int m) {if(n<0||m<0||n<m) return 0;return fac[n]*inv[m]%p*inv[n-m]%p;
}
signed main() {cin>>n>>m;init(600005);for(int k=0; k<=n; k++) ans=(ans+(C(m,k)*C(m,k)%p-C(m+1,k)*C(m-1,k)%p+p)%p*C(2*m+n-2*k+1,n-2*k+1)%p)%p;cout<<ans;return 0;
}
解法二
对 \(a\) 序列进行差分,得到长度为 \(n + 1\) 的序列 \(b\)。限制变为 \(b\) 的任意前缀和都 \(\geq 0\),且 \(b\) 中所有数之和 \(= 0\),绝对值之和 \(= 2m + 1\)。
将 \(b_1\) 改为 \(b_1 + 1\),限制变为 \(b\) 的任意前缀和都 \(> 0\),且 \(b\) 中所有数之和 \(= 1\),绝对值之和 \(= 2m + 1\)。
-
Raney 引理:如果 \(x_1,x_2,\cdots,x_k\) 是一个和为 \(1\) 的整数序列,则其所有循环位移中恰好有一个满足所有的前缀和都是正数。
-
证明:考虑重复这个序列,生成一个无限数列。这个数列的“平均斜率”是 \(\frac{1}{k}\),并且整个图象会被夹在两条斜率为 \(\frac{1}{k}\) 的直线之间,并且每条直线与图象恰好有一个切点(因为直线在每个周期内与整点只接触一次)。循环位移的起始点能且只能取下界直线的切点。
因此只需统计和为 \(1\) 且绝对值之和为 \(2m + 1\) 的序列个数,最后除以 \(n + 1\) 即可。枚举负数的个数 \(i\),方案数是:
神奇的是,两种做法推出来的式子完全不一样,但却是相等的。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e5+5,p=1e9+7;
int n,m,ans=0,fac[N],inv[N];
int qpow(int a,int b) {int ans=1;while(b) {if(b&1) ans=ans*a%p;a=a*a%p;b>>=1;}return ans;
}
void init(int lim) {fac[0]=inv[0]=1;for(int i=1; i<=lim; i++) fac[i]=fac[i-1]*i%p;inv[lim]=qpow(fac[lim],p-2);for(int i=lim-1; i>=1; i--) inv[i]=inv[i+1]*(i+1)%p;
}
int C(int n,int m) {if(n<0||m<0||n<m) return 0;return fac[n]*inv[m]%p*inv[n-m]%p;
}
signed main() {cin>>n>>m;init(m+n+1);for(int i=0; i<=n+1; i++) ans=(ans+C(m-1,i-1)*C(m+1+n-i,n-i)%p*C(n+1,i)%p)%p;cout<<ans*qpow(n+1,p-2)%p;return 0;
}
闲话:感觉解法一更加值得学习一点,解法二的引理真的会考吗/oh/oh
T4
原题:P8415。
还不会。