【题解】做题记录(2022.8)

(之前的就暂时不补了就从以后的开始记)

8.12

*CF1606D Red-Blue Matrix

题目分析:
我们考虑如果只有一列的情况,那么我们将这一列的元素从大到小排序之后,显然我们的红色行是下面连续的一段。
那么我们就考虑将我们的矩阵按第一列排序,显然因为第一列的限制,我们的红色行也只能是下面连续的一段,然后就直接枚举选哪一段,以及左右矩阵的分割点是哪个就好了。
可以预处理出来矩阵的各个部分的最大值与最小值就可以实现 \(O(1)\) 的复杂度判断是否合法。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
vector<int> a[MAXN];
vector<int> p1[MAXN],p2[MAXN],p3[MAXN],p4[MAXN];
int n,m;
int p[MAXN];
char ans[MAXN];
bool flag[MAXN];
bool cmp(int l,int r){
	return a[l][1] < a[r][1];
}
void work(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<=n+1;i++){
        p1[i].clear();p2[i].clear();p3[i].clear();p4[i].clear();a[i].clear();
		p1[i].resize(m+5,0);p2[i].resize(m+5,(int)1e6);p3[i].resize(m+5,(int)1e6);p4[i].resize(m+5,0);a[i].resize(m+5,0);
	}
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
    for(int i=0; i<=n+1; i++)	p[i] = i,flag[i] = false;
    sort(p+1,p+1+n,cmp);
    for(int i=1;i<=n;i++)   //左上角到左下角的最大值 
        for(int j=1;j<=m;j++){
            p1[p[i]][j]=a[p[i]][j];
            p1[p[i]][j]=max(p1[p[i]][j],p1[p[i-1]][j]);
            p1[p[i]][j]=max(p1[p[i]][j],p1[p[i]][j-1]);
        }
    for(int i=1;i<=n;i++)   //左下角到右上角的左小指 
        for(int j=m;j>=1;j--){
            p2[p[i]][j]=a[p[i]][j];
            p2[p[i]][j]=min(p2[p[i]][j],p2[p[i-1]][j]);
            p2[p[i]][j]=min(p2[p[i]][j],p2[p[i]][j+1]);
        }
    for(int i=n;i>=1;i--)   //右上角到左下角的最小值 
        for(int j=1;j<=m;j++){
            p3[p[i]][j]=a[p[i]][j];
            p3[p[i]][j]=min(p3[p[i]][j],p3[p[i+1]][j]);
            p3[p[i]][j]=min(p3[p[i]][j],p3[p[i]][j-1]);
        }
    for(int i=n;i>=1;i--)  //右下角到左上角的最大值 
        for(int j=m;j>=1;j--){
            p4[p[i]][j]=a[p[i]][j];
            p4[p[i]][j]=max(p4[p[i]][j],p4[p[i+1]][j]);
            p4[p[i]][j]=max(p4[p[i]][j],p4[p[i]][j+1]);
        }
    for(int i=2; i<=n; i++){
        for(int j=1;j<=m-1;j++){
            if(p3[p[i]][j]>p1[p[i-1]][j]&&p2[p[i-1]][j+1]>p4[p[i]][j+1]){
                for(int k=n;k>=i;k--) flag[p[k]] = true;
                printf("YES\n");
                for(int k=1;k<=n;k++){
                	if(flag[k])	printf("R");
                	else	printf("B");
				}
                printf(" %d\n",j);
                return;
            }
        }
    }
    printf("NO\n");
}
int main(){
    int t;
    scanf("%d",&t);
    while(t--){
    	work();
	}
    return 0;
}

总结:
考虑解决小的问题,并由小的问题推广到大问题,有利于发现题目性质。并且要善于观察数据范围。

*CF1606E Arena

题目分析:
我们读完题之后会发现英雄能不能全部干掉与最大值密切相关,那么我们就可以考虑设 \(f(i,j)\),表示有 \(i\) 个英雄,最大血量为 \(j\) 的最后全部被干掉的方案数。
转移也是相当显然的:

\[f(i,j) = \sum_{k=1}^i \binom{i}{k}f(k,j-(i-1)) \times (i-1)^{i-k} \]

也就是我们直接枚举这一轮剩下多少人,显然剩下的最大值就是 \(j-(i-1)\),而这一轮干掉的 \(i-k\) 个人显然血量为 \([1,i-1]\) 均可。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 505;
const int MOD = 998244353;
int dp[MAXN][MAXN],c[MAXN][MAXN],pre[MAXN][MAXN];
int power(int a,int b){
    int res = 1;
    while(b){
        if(b & 1)   res = (res * a)%MOD;
        a = (a * a)%MOD;
        b>>=1;
    }
    return res;
}
signed main(){
    int n,x;
    scanf("%lld%lld",&n,&x);
    c[0][0] = 1;
    for(int i=1; i<=n; i++){
        c[i][0] = 1;
        for(int j=1; j<=n; j++){
            c[i][j] = (c[i-1][j-1] + c[i-1][j])%MOD;
        }
    }
    for(int i=1; i<=500; i++){
        pre[i][0] = 1;
        for(int j=1; j<=500; j++){
            pre[i][j] = (pre[i][j-1] * i)%MOD;
        }
    }
    for(int i=2; i<=n; i++){
        for(int j=1; j<=x; j++){
            if(i-1 >= j)
                dp[i][j] = ((pre[j][i] - pre[j-1][i])%MOD + MOD) % MOD;
            else{
                for(int k=1; k<=i; k++){
                    dp[i][j] = (dp[i][j] + ((c[i][k] * dp[k][j - (i-1)])%MOD * pre[i-1][i-k])%MOD)%MOD;
                }
            }
        }
    }
    int ans = 0;
    for(int i=1; i<=x; i++) ans = (ans + dp[n][i])%MOD;
    printf("%lld\n",ans);
    return 0;
}

可以预处理出来需要的幂,可以省掉一个 \(O(\log n)\) 的复杂度

总结:
不能看到一道题就肯定他是某种题目,如果一直将这个题当作数学题做,显然做不出来,就像我一样。也要重充分发挥预处理的功能。

8.13

*CF1334E Divisor Paths

题目描述:
给定一个无向图,图中所有的点的编号均为 \(D\) 的约数,若 \(x,y\) 之间存在路径,当且仅当存在质数 \(p\) 使得 \(x \times p = y\),这条路径的权值为 \(f(y) - f(x)\)。定义 \(f(x)\) 为数 \(x\) 的约数个数。
给定 \(q\) 次询问,每次询问 \(u,v\) 两点间的最短路的条数。

题目分析:
显然我们选择 \(x \to \gcd(x,y) \to y\) 是最优的,因为这样使得我们的约数的个数变化尽可能地小。
那么就考虑如何走的问题了:我们会发现走其实就是在增加/删除约数,而增加/删除约数的顺序无所谓,所以方案数就是相当于一个可重元素的排列数。
先考虑一边:\(x \to \gcd(x,y)\)。我们对 \(\dfrac{x}{\gcd(x,y)}\) 进行质因数分解。
那么显然路径条数就是 \(\dfrac{cnt!}{\prod_{\text{v is prime}}cnt_v!}\)
\(cnt\) 代表能删除的个数,也就是质因数分解之后指数的和,\(cnt_v\) 则是质因数分解之后 \(v\) 的指数的和。
对于走 \(lcm\) 会不会更优,不行就都走一遍,但也可以证明走 \(lcm\) 确实不如走 \(gcd\) 优。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MOD = 998244353;
const int MAXN = 3e6+5;
int tot,fac[MAXN],prime[MAXN];
int mod(int x){
	return x%MOD;
}
int power(int a,int b){
	int res = 1;
	while(b){
		if(b & 1)	res = mod(res * a);
		a = mod(a * a);
		b >>= 1;
	}
	return res;
}
void pre_work(){
	fac[0] = fac[1] = 1;
	for(int i=2; i<=100; i++)	fac[i] = mod(fac[i-1] * i);
}
void fenjie(int x){
	for(int i=2; i * i <= x; i++){
		if(x % i == 0){
			prime[++tot] = i;
			while(x % i == 0){
				x /= i;
			}
		}
	}
	if(x > 1)	prime[++tot] = x; 
}
int gcd(int a,int b){
	if(b == 0)	return a;
	return gcd(b,a%b);
}
int solve(int x){
	int a = 1,b = 1,cnt = 0;
	for(int i=1; i<=tot; i++){
		int tmp = 0;
		while(x % prime[i] == 0){
			x /= prime[i];tmp++;cnt++;
		}
		b = mod(b * fac[tmp]);
	}
	a = fac[cnt];
	return mod(a * power(b,MOD-2));
}
signed main(){
	pre_work();
	int d;
	scanf("%lld",&d);
	fenjie(d);
	int q;
	scanf("%lld",&q);
	while(q--){
		int u,v;
		scanf("%lld%lld",&u,&v);
		int h = gcd(u,v);
		printf("%lld\n",mod(solve(u/h) * solve(v/h)));
	}
	return 0;
}

总结:
这类题走肯定都是有规律的,要发现最优的道路是怎么走的,然后再去考虑如何统计方案数

ABC264 E

题目分析:
显然的套路:将删边反着来就成了建边。
那么就维护一个并查集,合并的时候判断一下是否新的城市有电了就好了

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 6e6+5;
struct edge{
	int from,to;
}e[MAXN];
struct node{
	int val,id;
}a[MAXN];
bool cmp(node l,node r){
	return l.id > r.id;
}
int fa[MAXN],sz[MAXN],res,ans[MAXN];
bool tag[MAXN],flag[MAXN];
int find(int x){
	if(fa[x] == x)	return x;
	return fa[x] = find(fa[x]);
}
void merge(int x,int y){
	x = find(x),y = find(y);
	if(x == y)	return;
	if(!tag[x] && tag[y])	res += sz[x],tag[x] = true;
	if(!tag[y] && tag[x])	res += sz[y],tag[y] = true;
	sz[x] = sz[x] + sz[y];
	fa[find(y)] = find(x);
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,m,k;
	cin>>n>>m>>k;
	for(int i=1; i<=k; i++){
		cin>>e[i].from>>e[i].to;
	}
	for(int i=n+1; i<=n+m + 1; i++)	tag[i] = true;
	for(int i=1; i<=n+m + 1; i++)	fa[i] = i,sz[i] = 1;
	int q;
	cin>>q;
	for(int i=1; i<=q; i++){
		cin>>a[i].val;a[i].id = i;
		flag[a[i].val] = true;
	}
	sort(a+1,a+q+1,cmp);
	for(int i=1; i<=k; i++){
		if(!flag[i]){
			merge(e[i].from,e[i].to);
		}
	}
	for(int i=1; i<=q; i++){
		int now = a[i].val;
		ans[a[i].id] = res;
		merge(e[now].from,e[now].to);
	}
	for(int i=1; i<=q; i++){
		cout<<ans[i]<<endl;
	}
	return 0;
}

总结:
非常经典的套路,将询问离线反着考虑,往往可以省很多事

CF1712B Woeful Permutation

题目分析:
显然任意两个相邻的数互质,那么我们显然可以考虑错位排列或者交换相邻的数,显然这里交换相邻的数更优

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		int n;
		scanf("%d",&n);
		if(n % 2 == 1){
			printf("1 ");
			for(int i=2; i<=n; i+=2){
				printf("%d %d ",i+1,i);
			}
		}
		else{
			for(int i=1; i<=n; i+=2){
				printf("%d %d ",i+1,i);
			}
		}
		printf("\n");
	}
	return 0;
}

总结:
非常好用的性质:差为 \(1\) 的两个数必然互质

*CF1712D Empty Graph

题目分析:
我们先考虑 \(d(u,v)\) 是什么,显然 \(d(u,v) = \min(\min(a_l,\cdots,a_r),2 \times \min(a_1,a_2,\cdots,a_n))\)
我们从 \(u\) 走到 \(v\) 共有两条路:直接走 \(u \to v\),从 \(u\)\(v\) 向外走到某个不是 \(u,v\) 的点,然后直接走到另一个点。这两种走法也就是对应着 \(d(u,v)\) 的两个部分。
下面就考虑图的直径怎么求:一个显然的结论,图的直径肯定是 \(\max(d(i,i+1))\)。我们可以考虑上面的式子,显然每多一个点对于第一部分就多一个限制,所以最少的时候有两个点就是最优的。
也就是说我们的直径的长度可以化简为:\(\min\bigg(\max_{i=1}^{n-1}\big(\min(a_i,a_{i+1})\big),2 \times \min(a_1,a_2,\cdots,a_n)\bigg)\)
看到最小值最大那么就考虑二分答案,那么就做完了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int MAXN = 2e6+5;
int n,k;
int a[MAXN],b[MAXN];
bool check(int x){
	int cnt = k;
	for(int i=1; i<=n; i++){
		b[i] = a[i];
		if(b[i] * 2 < x){
			if(!cnt)	return false;
			b[i]=INF;cnt--; 	
		}
	}
	if(cnt >= 2)	return INF;
	else if(cnt == 1){
		for(int i=1; i<n; i++){
			if(max(b[i],b[i+1]) >= x)	return true;
		}
		return false;
	}
	else{
		for(int i=1; i<n; i++){
			if(min(b[i],b[i+1]) >= x)	return true;
		}
		return false;
	}
}
void solve(){
	int l = 0,r = INF;
	while(l < r){
		int mid = (l + r + 1)>>1;
		if(check(mid))	l = mid;
		else	r = mid - 1;
	}
	printf("%d\n",l);
}
int main(){
	int t;
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&k);
		for(int i=1; i<=n; i++){
			scanf("%d",&a[i]);
		}
		solve();
	}
	return 0;
}

总结:
要从小问题开始解决,将题目里给的东西一点点弄,直到最后就可以显然地弄出来了

8.21

*[NOI2001] 陨石的秘密

题目分析:
每一个合法的括号序列 S 可以唯一地表示为 A(B) 或 A[B] 或 A{B}。
所以我们可以直接枚举 A 和 B 分别是什么,然后在 B 的两边加入这个括号就好了。
也就是使用 DP 来做,设 \(dp[i][a][b][c]\) 表示深度不超过 \(i\),使用了 \(a\) 个 {},\(b\) 个 [],\(c\) 个 () 的方案数。
代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int D = 35;
const int N = 15;
const int MOD = 11380;
int dp[D][N][N][N];
int mod(int x){return ((x % MOD)+MOD)%MOD;}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int l1,l2,l3,d;
	scanf("%lld%lld%lld%lld",&l1,&l2,&l3,&d);
	if(d == 0){
		if(l1 == 0 && l2 == 0 && l3 == 0)
			printf("%lld\n",1);
		else
			printf("%lld\n",0);
		return 0;
	}
	dp[0][0][0][0] = 1;
	for(int i=1; i<=d; i++){
		dp[i][0][0][0] = 1;
		for(int a=0; a<=l1; a++){
			for(int b=0; b<=l2; b++){
				for(int c=0; c<=l3; c++){
					//现在开始更新 dp[i][a][b][c]
					for(int j=0; j<=c-1; j++){  //直接枚举 B 里面有多少个 () 
						dp[i][a][b][c] += mod(dp[i-1][0][0][j] * dp[i][a][b][c-1-j]);
					}
					for(int j=0; j<=b-1; j++){  //直接枚举 B 里面有多少个[] 多少个 () 
						for(int k=0; k<=c; k++){
							dp[i][a][b][c] += mod(dp[i-1][0][j][k] * dp[i][a][b-j-1][c-k]);
						}
					}
					for(int j=0; j<=a-1; j++){
						for(int k=0; k<=b; k++){
							for(int h=0; h<=c; h++){
								dp[i][a][b][c] += mod(dp[i-1][j][k][h] * dp[i][a-j-1][b-k][c-h]);
							}
						}
					}
//					printf("dp[%lld][%lld][%lld][%lld] = %lld\n",i,a,b,c,dp[i][a][b][c]);
				}
			}
		}
	}
	printf("%lld\n",mod(dp[d][l1][l2][l3] - dp[d-1][l1][l2][l3]));
	return 0;
}

总结:
每个合法的括号序列,一定可以唯一地分解为 A[B] 或 A{B} 或 A(B)

[IOI2019]排列鞋子

题目分析:
显然地贪心:每个数选择距离它最近的匹配的数。
可以证明选择一个更远的必然不比选择一个更近的更优。
所以每个数记录一下就好了,找一个指针什么的瞎搞一下就可以了。
代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
int a[N],tree[4 * N],fir[N + N],last[N + N],nxt[N + N];
bool vis[N + N];
void pushup(int now){
	tree[now] = tree[now<<1] + tree[now<<1|1];
}
void change(int now,int now_l,int now_r,int pos,int val){
	if(now_l == now_r){
		tree[now] += val;
		return;
	}
	int mid = (now_l + now_r)>>1;
	if(pos <= mid)	change(now<<1,now_l,mid,pos,val);
	else	change(now<<1|1,mid+1,now_r,pos,val);
	pushup(now);
}
int query(int now,int now_l,int now_r,int l,int r){
	if(l <= now_l && r >= now_r){
		return tree[now];
	}
	int mid = (now_l + now_r)>>1;
	int ans = 0;
	if(l <= mid)	ans += query(now<<1,now_l,mid,l,r);
	if(r > mid)		ans += query(now<<1|1,mid+1,now_r,l,r);
	return ans;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%lld",&n);n*=2;
	int mx = 0;
	for(int i=1; i<=n; i++){
		scanf("%lld",&a[i]);
		mx = max(mx,a[i]);
	}
	for(int i=n; i>=1; i--){
		if(last[a[i] + mx])	nxt[i] = last[a[i] + mx];
		last[a[i] + mx] = i;
	}
	for(int i=1; i<=n; i++){
		if(!fir[a[i] + mx])	fir[a[i] + mx] = i;
//		printf("nxt[%lld] = %lld\n",i,nxt[i]);
	}
	int ans = 0;
	for(int i=1; i<=n; i++){
		if(vis[i])	continue;
		vis[i] = true;
		while(vis[fir[-a[i] + mx]])	fir[-a[i]+mx] = nxt[fir[-a[i] + mx]];
		int to = fir[-a[i] + mx];
		vis[to] = true;change(1,1,n,to,1);
		ans += to - i - 1 - query(1,1,n,i+1,to-1);
		if(a[i] > 0)	ans++;
	}
	printf("%lld\n",ans);
	return 0;
}

8.23

[TJOI2013] 奖学金

题目分析:
显然我们需要先按成绩排序。
那么假设我们需要选择 \(i\) 作为中位数,那么意味着要在 \([1,i-1]\) 里选 \(\dfrac{n-1}{2}\) 个,要在 \([i+1,n]\) 里选 \(\dfrac{n-1}{2}\) 个,那么预处理出来 \(f[i]\) 表示 \([1,i]\) 里选 \(\dfrac{n-1}{2}\) 个的最小花费,\(g[i]\) 表示在 \([i,n]\) 里选 \(\dfrac{n-1}{2}\) 个的最小花费,答案显然就是 \(min(f[i-1] + c[i] + g[i+1])\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5+5;
const int INF = 1e18+5;
struct node{
	int a,b;
}p[N];
int f[N],g[N];
bool cmp_a(node l,node r){
	return l.a < r.a;
}
signed main(){
	int n,c,k;
	scanf("%lld%lld%lld",&n,&c,&k);
	for(int i=1; i<=c; i++)	scanf("%lld%lld",&p[i].a,&p[i].b);
	sort(p+1,p+c+1,cmp_a);
	//预处理出来两个值,f[i] 表示前 i 个选 (n-1)/2 的最小花费
	//g[i] 表示后 i 个选 (n-1)/2 的最小花费
	int sum = 0;
	priority_queue<int> q;
	for(int i=1; i<=c; i++){
		if(q.size() < (n-1)/2){
			q.push(p[i].b);
			sum+=p[i].b;
		}
		else if(q.top() > p[i].b){
			sum += p[i].b - q.top();q.pop();
			q.push(p[i].b);
		}
		f[i] = sum;
	}  
	while(!q.empty())	q.pop();
	sum = 0;
	for(int i=c; i>=1; i--){
		if(q.size() < (n-1)/2){
			q.push(p[i].b);
			sum+=p[i].b;
		}
		else if(q.top() > p[i].b){
			sum += p[i].b - q.top();q.pop();
			q.push(p[i].b);
		}
		g[i] = sum;
	}
	int ans = -INF;
	for(int i=(n-1)/2+1; i<=c-(n-1)/2; i++){
		if(p[i].b + f[i-1] + g[i+1] < k){
			ans = max(ans,p[i].a);
		}
	}
	if(ans == -INF)	printf("-1");
	else	printf("%lld\n",ans);
	return 0;
}

总结:
预处理前缀和后缀的信息,然后通过前缀后缀的合并得到答案

[SHOI2013]发微博

题目分析:
我们显然把所有的操作逆序一下,这样各种操作就很简单了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;
const int M = 1e6+5;
struct node{
	char opt;
	int l,r;
}q[M];
int cnt[N],ans[N];
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++){
		cin>>q[i].opt;
		if(q[i].opt != '!'){
			cin>>q[i].l>>q[i].r;
		}
		else{
			cin>>q[i].l;
		}
	}
	for(int i=m; i>=1; i--){
		if(q[i].opt == '!')	cnt[q[i].l]++;
		else if(q[i].opt == '-'){
			ans[q[i].l] -= cnt[q[i].r];
			ans[q[i].r] -= cnt[q[i].l];
		}
		else{
			ans[q[i].l] += cnt[q[i].r];
			ans[q[i].r] += cnt[q[i].l];
		}
	}
	for(int i=1; i<=n; i++){
		printf("%d ",ans[i]);
	}
	return 0;
}

总结:
逆序进行操作

教主的魔法

题目分析:
我们一眼感觉像是线段树,但是想想仿佛做不到。
那么就考虑大力分块,每个块排个序就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6+5;
int n,q,S,tot,bl[MAXN],br[MAXN],sa[MAXN],a[MAXN],tag[MAXN];
void sort_block(int x){
	for(int i=bl[x]; i<=br[x]; i++)
		sa[i] = a[i];
	sort(sa+bl[x],sa+br[x]+1);
}
void prework(){
	for(int i=1; i<=n; i++){
		if(i % S == 1){
			br[tot] = i-1;
			bl[++tot] = i;
		}
	}
	br[tot] = n;
	br[tot+1] = bl[tot+1] = n+1;
	for(int i=1; i<=tot; i++)
		sort_block(i);
}
void add_val(int l,int r,int val){
	int L = (l-1) / S + 1,R = (r-1) / S + 1;
	if(r - l + 1 <= 2 * S){
		for(int i=l; i<=r; i++){
			a[i]+=val;
		}
		for(int i=L; i<=R; i++)
			sort_block(i);
	}
	else{
		bool tagl = true,tagr = true;
		if(l == bl[L]){
			tagl = false;
			L--;
		}
		if(r == br[R]){
			tagr = false;
			R++;
		}
		for(int i = L+1; i<=R-1; i++){
			tag[i]+=val;
		}
		for(int i=l; i<=br[L]; i++){
			a[i]+=val;
		}
		if(tagl)	sort_block(L);
		for(int i=bl[R]; i<=R; i++){
			a[i]+=val;
		}
		if(tagr)	sort_block(R);
	}
}
int find(int x,int val){
	int l = bl[x],r = br[x],ans = br[x] + 1,mid = (l+r)>>1;
	while(l <= r){
		mid = (l+r)>>1;
		if(sa[mid] >= val){
			ans = mid;
			r = mid-1;
		}
		else{
			l = mid+1;
		}
	}
	return br[x] - ans + 1;
}
int query(int l,int r,int val){
	int L = (l - 1) / S + 1,R = (r - 1) / S + 1,ans = 0;
	if(r - l + 1 <= 2 * S){
		for(int i=l; i<=r; i++){
			if(a[i] + tag[(i-1) / S + 1] >= val)
				ans++;
		}
		return ans;
	}
	else{
		if(l == bl[L])	L--;
		if(r == br[R])	R++;
		for(int i=L+1; i<=R-1; i++){
			ans += find(i,val - tag[i]);
		}
		for(int i=l; i<=br[L]; i++){
			if(a[i] + tag[L] >= val)
				ans++;
		}
		for(int i = bl[R]; i<=r; i++){
			if(a[i] + tag[R] >= val){
				ans++;
			}
		}
		return ans;
		
	}
}
int main(){
	cin>>n>>q;
	for(int i=1; i<=n; i++){
		cin>>a[i];
	}
	S = sqrt(n);
	prework();
	while(q--){
		char opt;
		int l,r,x;
		cin>>opt>>l>>r>>x;
		if(opt == 'M'){
			add_val(l,r,x);
		}
		else if(opt == 'A'){
			printf("%d\n",query(l,r,x));
		}
	}
	return 0;
} 

总结:
对于传统数据结构难以维护的东西,考虑分块、莫队等数据结构

逃离僵尸岛

题目分析:
显然就是从感染的点开始跑一下 bfs 把危险的城市搞出来,然后将点权放到连向它的边上,直接跑最短路就好了。
注意:建双向边的边权不同

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6+5;
const int M = 6e6+5;
const int INF = 1e18+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}a[M],e[M];
int cnt,n,m,k,s,P,Q,c[N],flag[N],dis[N],head[N];
bool vis[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
}
void bfs(){
	queue<pair<int,int> > q;
	for(int i=1; i<=k; i++){
		q.push({c[i],0});vis[c[i]] = true;flag[c[i]] = INF;
	}
	while(!q.empty()){
		pair<int,int> tmp = q.front();q.pop();
		int now = tmp.first;
		if(tmp.second >= s)	continue;
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(!vis[to] && tmp.second + 1 <= s){
				vis[to] = true;
				q.push({to,tmp.second+1});
				flag[to] = Q;
			}
		}
	}
}
int dij(){
	memset(dis,0x3f,sizeof(dis));memset(vis,false,sizeof(vis));
	priority_queue<pair<int,int> > q;
	dis[1] = 0;q.push({0,1});
	while(!q.empty()){
		int now = q.top().second;q.pop();
		if(vis[now])	continue;
		vis[now] = true;
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(!vis[to] && dis[to] > dis[now] + e[i].val){
				dis[to] = dis[now] + e[i].val;
				q.push({-dis[to],to});
			}
		}
	}
	return dis[n];
}
signed main(){
	scanf("%lld%lld%lld%lld%lld%lld",&n,&m,&k,&s,&P,&Q);
	for(int i=1; i<=n; i++)	flag[i] = P;
	for(int i=1; i<=k; i++)	scanf("%lld",&c[i]);
	for(int i=1; i<=m; i++){
		scanf("%lld%lld",&a[i].nxt,&a[i].to);
		add_edge(a[i].nxt,a[i].to,0);add_edge(a[i].to,a[i].nxt,0);
	}
	bfs();
	memset(head,0,sizeof(head));cnt = 0;
	flag[n] = 0;
	for(int i=1; i<=m; i++){
		add_edge(a[i].nxt,a[i].to,flag[a[i].to]);add_edge(a[i].to,a[i].nxt,flag[a[i].nxt]);
	}  //注意双向边的边权不一样 
	printf("%lld\n",dij());
	return 0;
}

总结:
点权与边权的互换

8.27

*[JSOI2015]圈地

题目分析:
看了标签才意识到需要网络流
显然这是一个最小割模型,建图也是很简单:

  • 相邻的格子建边,流量为中间的墙的花费
  • 南南想要的格子与源点建边,流量为收益
  • 强强想要的格子与汇点建边,流量为收益

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int M = 4e5+5;
const int N = 2e5+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M];
int cnt = 1,s,t,n,m,head[N],dis[N],cur[N];
int number(int i,int j){
	return (i-1) * m + j;
}
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,0,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;
	q.push(s);dis[s] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(e[i].val && !dis[to]){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	}
	return dis[t] != 0;
}
int dfs(int now,int limit){
	if(now == t)	return limit;
	int flow = 0;
	for(int &i = cur[now]; i && flow < limit; i = e[i].nxt){
		int to = e[i].to;
		if(e[i].val && dis[to] == dis[now] + 1){
			int h = dfs(to,min(limit-flow,e[i].val));
			e[i].val -= h;e[i^1].val += h;flow += h;
			if(!h)	dis[to] = INF;
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(s,INF)){
			ans += flow;
		}
	}
	return ans;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld",&n,&m);
	s = n * m + 1,t = s + 1;
	int sum = 0;
	for(int i=1; i<=n; i++){
		for(int j=1; j<=m; j++){
			int val;
			scanf("%lld",&val);
			if(val > 0){
				sum += val,add_edge(s,number(i,j),val);
			}
			if(val < 0){
				sum += -val,add_edge(number(i,j),t,-val);
			}
		}
	}
	for(int i=1; i<n; i++){
		for(int j=1; j<=m; j++){
			int val;
			scanf("%lld",&val);
			add_edge(number(i,j),number(i+1,j),val);
			add_edge(number(i+1,j),number(i,j),val);
		}
	}
	for(int i=1; i<=n; i++){
		for(int j=1; j<m; j++){
			int val;
			scanf("%lld",&val);
			add_edge(number(i,j),number(i,j+1),val);
			add_edge(number(i,j+1),number(i,j),val);
		}
	}
	printf("%lld\n",sum - dinic());
	return 0;
}

总结:
对于本题的数据范围想到图论建模应该是很显然的。
对于几百的数据范围一般情况下就是图论建模或者是区间 \(dp\) 或者高斯消元。

[JSOI2015]字符串树

题目分析:
一眼就是可持久化 Trie 树,然后就显然可以过。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int ALP = 27;
const int N = 1e6+5;
struct edge{
	int nxt,to;
	string val;
	edge(){}
	edge(int _nxt,int _to,string _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[N];
int n,tot,res,head[N],ch[N][ALP],cnt[N],fa[N][24],dep[N],rt[N];
void add_edge(int from,int to,string val){
	e[++res] = edge(head[from],to,val);
	head[from] = res;
}
int newnode(int lst){
	++tot;
	for(int i=0; i<26; i++)	ch[tot][i] = ch[lst][i];
	cnt[tot] = cnt[lst];
	return tot;
}
void Insert(int lst,int &now,string s){
	if(!now)	now = newnode(lst);
	int cur = now;
	int n = s.size();
	for(int i=0; i<n; i++){
		if(ch[lst][s[i] - 'a'])	ch[cur][s[i] - 'a'] = newnode(ch[lst][s[i] - 'a']);
		else	ch[cur][s[i] - 'a'] = ++tot;
		cur = ch[cur][s[i] - 'a'];cnt[cur]++;lst = ch[lst][s[i] - 'a'];
	}
}
int query(int now,string s){
	now = rt[now];
	int n = s.size();
	for(int i=0; i<n; i++){
		now = ch[now][s[i] - 'a'];
	}
	return cnt[now];
}
void dfs(int now,int fath){
	for(int i = head[now]; i; i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dep[to] = dep[now] + 1;fa[to][0] = now;
		Insert(rt[now],rt[to],e[i].val);
		dfs(to,now);
	}
}
void pre_lca(){
	for(int i=1; i<=22; i++){
		for(int j=1; j<=n; j++){
			fa[j][i] = fa[fa[j][i-1]][i-1];
		}
	}
}
int get_lca(int x,int y){
	if(dep[x] > dep[y])	swap(x,y);
	for(int i=22; i>=0; i--){
		if(dep[fa[y][i]] >= dep[x]){
			y = fa[y][i];
		}
	}
	if(y == x)	return x;
	for(int i=22; i>=0; i--){
		if(fa[x][i] != fa[y][i]){
			y = fa[y][i];
			x = fa[x][i];
		}
	}
	return fa[x][0];
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	cin>>n;
	for(int i=1; i<n; i++){
		int from,to;
		string val;
		cin>>from>>to>>val;
		add_edge(from,to,val);add_edge(to,from,val);
	}
	tot = 1;rt[1] = 1;dep[1] = 1;
	dfs(1,-1);
	pre_lca();
	int q;
	scanf("%d",&q);
	while(q--){
		int x,y;
		string s;
		cin>>x>>y>>s;
		int lca = get_lca(x,y);
		printf("%d\n",query(x,s) + query(y,s) - 2 * query(lca,s));
	}
	return 0;
}

总结:
对于字符串算法,不能只有 SAM,AC 自动机等,还要想想更基本的这些。

*[JSOI2012]始祖鸟

题目分析:
这题竟然是个高斯消元
我们设 \(x_i = 1\) 表示 \(i\) 去上游,\(x_i = 0\) 表示 \(i\) 去下游。
对于有偶数个朋友的鸟来说:\(x_{a_1} \oplus x_{a_2} \oplus \cdots \oplus x_{a_n} = 0\),也就是说有偶数个朋友去了上游。
对于有奇数个朋友的鸟来说:\(x_{a_1} \oplus x_{a_2} \oplus \cdots \oplus x_{a_n} \oplus x_i = 1\),可以分类讨论当前选择上游还是下游就发现正确性显然。
但是高斯消元是 \(O(n^3)\),但是我们发现这些值只有 \(0\)\(1\),所以就使用 bitset 优化就好了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e3+5;
int n,res;
bitset<N> a[N];
bool Gauss(){
	for(int k=1; k<=n; k++){
		bool flag = false;
		for(int i=k; i<=n; i++){
			if(a[i][k]){
				swap(a[i],a[k]);
				flag = true;
				break;
			}
		}
		if(!flag)	continue;
		for(int i=1; i<=n; i++){
			if(a[i][k] && i != k)	a[i] ^= a[k];
		}
	}
	for(int i=n; i>=1; i--){
		if(a[i][i]){
			res += a[i][n+1];
		}
		else if(a[i][n+1]){
			printf("Impossible\n");
			return false;
		}
	}
	return true;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%d",&n);
	for(int i=1; i<=n; i++){
		int m;scanf("%d",&m);
		if(m & 1)	a[i][i] = a[i][n+1] = 1;
		for(int j=1; j<=m; j++){
			int h;scanf("%d",&h);
			a[i][h] = 1;
		}
	}
	if(Gauss()){
		printf("%d\n",res);
		for(int i=1; i<=n; i++){
			if(a[i][n+1])	printf("%d ",i);
		}
	}
	return 0;
}

总结:
对于 bitset 的优化是一个很好用的东西,本题的数据范围显然高斯消元过不去,但是加上 bitset 优化之后就可以稳稳地过掉了

8.30

[POJ3281]餐饮

题目分析:
显然可以讲源点向饮料连边,汇点向食物连边,因为每种只有一个所以流量为一。
我们将每头牛与他喜欢的饮料与食物连边,并且因为每头牛限制只允许选择一个所以将牛拆点,流量为一。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
const int M = 2e6+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M];
int s,t,cnt=1,cur[N],head[N],dis[N];
bool vis[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);
	head[to] = cnt;
}
bool bfs(){
	memset(vis,false,sizeof(vis));memset(dis,-1,sizeof(dis));
	memcpy(cur,head,sizeof(head));
	queue<int> q;
	q.push(s);dis[s] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			if(e[i].val && dis[to] == -1){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	}
	return dis[t] != -1;
}
int dfs(int now,int limit){
	if(now == t)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		cur[now] = i;
		int to = e[i].to;
		if(dis[to] == dis[now] + 1 && e[i].val){
			int h = dfs(to,min(limit - flow,e[i].val));
			if(!h)	dis[to] = -1;
			e[i].val -= h; e[i^1].val += h;flow += h;
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(s,INF)){
			ans += flow;
		}
	}
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n,f,d;
	scanf("%d%d%d",&n,&f,&d);
	s = n * 2 + f + d + 1,t = s + 1;
	for(int i=1; i<=f; i++)	add_edge(s,i,1);
	for(int i=1; i<=d; i++)	add_edge(i + f,t,1);
	for(int i=1; i<=n; i++){
		int a,b,c;
		scanf("%d%d",&a,&b);
		for(int j=1; j<=a; j++){
			scanf("%d",&c);
			add_edge(c,f + d + i,1);
		}
		for(int j=1; j<=b; j++){
			scanf("%d",&c);
			add_edge(f + d + n + i,f + c,1);
		}
		add_edge(f + d + i,f + d + n + i,1);
	}
	printf("%d",dinic());
	return 0;
} 

总结:
使用网络流解决匹配问题非常常用。

[网络流 \(24\) 题]最长不下降子序列问题

题目分析:
考虑对于第一问,我们显然可以通过 \(DP\) 求解,即设 \(dp[i]\) 表示以 \(i\) 个点为结尾的最长的不降子序列的长度。
这样转移就显然是:

\[dp[i] = \max_{j < i \and x[j] \le x[i]} dp[j] + 1 \]

对于第二问,我们考虑网络流的建模:
对于 \(i\),将满足 \(dp[j] + 1 = dp[i]\)\(j\)\(i\) 连边,因为每个点只能选择一次,所以流量为 \(1\)
但是我们发现这样仍然无法限制住每个点只能选一次的条件,所以考虑拆点,将两个点之间连接边流量 \(1\)

我们设第一问的答案为 \(s\)
则将满足 \(dp[i] = 1\)\(i\)\(S\) 连边,将满足 \(dp[i] = s\)\(i\)\(T\) 连边,流量均为 \(1\)
可以发现,我们的每一点流量一定是对应着一条不同的长度为 \(s\) 的不降子序列,所以跑一遍最大流就好了。
对于第三问,我们发现就是去掉了对 \(1\)\(n\) 的限制,所以他们的流量限制放开就好了。

注意:如果 \(n\)\(T\) 之间没有边,那么不能强行加边,如果有边才能设为 \(+\infty\),此处就需要好好理解题意,就能明白了。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
const int M = 2e6+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M];
int cnt = 1,S,T,head[N],dis[N],a[N],cur[N],f[N];
void add(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,-1,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;
	q.push(S);dis[S] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i; i =e[i].nxt){
			int to = e[i].to;
			if(e[i].val && dis[to] == -1){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	} 
	return dis[T] != -1;
}
int dfs(int now,int limit){
	if(now == T)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		cur[now] = i;
		int to = e[i].to;
		if(dis[to] == dis[now] + 1 && e[i].val){
			int h = dfs(to,min(limit - flow,e[i].val));
			if(!h)	dis[to] = INF;
			e[i].val -= h;e[i^1].val += h;flow += h;
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(S,INF)){
			ans += flow;
		}
	}
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int n;
	scanf("%d",&n);
	S = n + n + 1,T = S + 1;
	for(int i=1; i<=n; i++){
		scanf("%d",&a[i]);
	}
	int s = -INF;
	for(int i=1; i<=n; i++){
		add(i,i+n,1);f[i] = 1;
		for(int j=1; j<i; j++){
			if(a[j] <= a[i])
				f[i] = max(f[i],f[j] + 1);
		}
		for(int j=1; j<i; j++){
			if(a[j] <= a[i] && f[i] == f[j] + 1){
				add(j+n,i,1);
			}
		}
		s = max(s,f[i]);
	}
	for(int i=1; i<=n; i++){
		if(f[i] == 1)	add(S,i,1);
		else if(f[i] == s)	add(i+n,T,1);
	}
	printf("%d\n",s);
	if(s == 1){  //一会看看需不需要特判 
		printf("%d\n%d\n",n,n); 
		return 0;
	}
	int ans = dinic();
	printf("%d\n",ans);
	for(int i=2; i<=cnt; i++){
		int from = e[i^1].to,to = e[i].to;
		if(from == S && to == 1)	e[i].val = INF;  //容量 -> INF 然后增广 
		if(from == 1 && to == 1 + n)	e[i].val = INF;
		if(from == n && to == n + n)	e[i].val = INF;
		if(from == n + n && to == T)	e[i].val = INF;
	}
	ans = ans + dinic();
	printf("%d\n",ans);
	return 0;
}

总结:
通过某个已知的算法或者数据结构的过程对我们当前问题的求解进行优化的思想需要多多考虑。
比如本题,将 \(dp\) 的过程进行转化为网络流模型。
以及比如将 \(dp\) 的转移的区间转化为线段树上的区间等等优化。

[POJ3498]企鹅游行

题目分析:
我们可以将企鹅当作流量然后来思考问题。

显然起跳限制我们可以通过拆点来限制,而对于集合地点我们发现仅仅通过一次网络流很难一次求解,那么我们就考虑枚举集合点,作为汇点。
网络流的建边就是:\(S\)\(i\) 连企鹅个数的流量,\(i\)\(j\) 若可以互达,那么就连接 \(+\infty\) 的流量,因为只有起跳限制而我们已经限制了。

那么求一遍最大流,如果大小够了就可以当作集合地。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 500;
const int M = 5e4+5;
const int INF = 1e9+5;
const double eps = 1e-6;
struct point{
	int x,y,n,m;
}p[N];
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M];
int S,T,cnt=1,head[N],cur[N],dis[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,-1,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;
	q.push(S);dis[S] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == -1 && e[i].val){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	}
	return dis[T] != -1;
}
int dfs(int now,int limit){
	if(now == T)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		cur[now] = i;
		int to = e[i].to;
		if(dis[to] == dis[now] + 1 && e[i].val){
			int h = dfs(to,min(limit - flow,e[i].val));
			e[i].val -= h; e[i^1].val += h; flow += h;
			if(!h)	dis[to] = INF;
		}
	}	
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(S,INF)){
			ans += flow;
		}
	}
	return ans;
}
double get_dis(point a,point b){
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int ti;
	scanf("%d",&ti);
	while(ti--){
//		printf("New_Test:\n");
		int n,sum = 0;
		double d;
		bool flag = false;
		scanf("%d%lf",&n,&d);  //仿佛不能用 %f 读入 
		for(int i=1; i<=n; i++){
			scanf("%d%d%d%d",&p[i].x,&p[i].y,&p[i].n,&p[i].m);
			sum += p[i].n;
		}
		for(T=1; T<=n; T++){  //枚举汇点(突然发现这种枚举方式好妙啊 
//			printf("T:%d\n",T);
			memset(head,0,sizeof(head));cnt = 1;
			for(int i=1; i<=n; i++){
				add_edge(S,i,p[i].n);
				add_edge(i,i+n,p[i].m);
				for(int j=1; j<i; j++){
					if(get_dis(p[i],p[j]) - d < -eps){
						add_edge(i+n,j,INF);
						add_edge(j+n,i,INF);
//						printf("%d %d\n",i,j);
					}
				}
			}
			int ans = dinic();
			if(ans == sum)
				printf("%d ",T-1),flag = true;
		}
		if(!flag)	printf("-1");
		printf("\n");
	}
	return 0;
}

总结
拆点:一个很基本的思想。以及将某个题目的量视作流量对题目进行分析,可以更容易地分析出来

[POJ1149] PIGS

题目分析:
这个题的建模是真的神奇。

显然我们可以将猪作为流量思考问题。

我们考虑对于猪舍 \(i\) 的猪第一次被使用,是在第一个有钥匙 \(i\) 的顾客 \(a\) 那里,而对于猪舍 \(i\) 的第 \(j\) 次使用,一定是在第 \(j-1\) 次使用之后转化过来的。也就是我们以顾客为点,对于第一次使用 \(S \to a\) 流量为猪舍 \(i\) 的猪的数量,而对于后面的几次使用,都从上一次使用来连边。

而对于顾客 \(i\) 来说,他就可以理解为可以随意地调换他有钥匙的那些猪舍,也就是那些猪舍都可以使用,就直接连边,流量为 \(+\infty\),显然也应该从他们向汇点连边,表示买猪。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
const int M = 1e6+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M];
int cnt=1,S,T,head[N],cur[N],dis[N],v[N],last[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,0);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,-1,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;
	q.push(S);dis[S] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i;i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == - 1 && e[i].val){
				dis[to] =dis[now] + 1;
				q.push(to); 
			}
		}
	}
	return dis[T] != -1;
}
int dfs(int now,int limit){
	if(now == T)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		cur[now] = i;
		int to = e[i].to;
		if(e[i].val && dis[to] == dis[now] + 1){
			int h = dfs(to,min(limit - flow,e[i].val));
			e[i].val -= h;e[i ^ 1].val += h;flow += h;
			if(!h)	dis[to] = INF;
		}
	}
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(S,INF)){
			ans += flow;
		}
	}
	return ans;
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	int m,n;
	scanf("%d%d",&m,&n);
	S = m + 1,T = S + 1;
	for(int i=1; i<=m; i++)	scanf("%d",&v[i]);
	for(int i=1; i<=n; i++){
		int sz;
		scanf("%d",&sz);
		for(int j=1; j<=sz; j++){
			int h;scanf("%d",&h);
			if(last[h])	add_edge(last[h],i,INF);
			else	add_edge(S,i,v[h]);
			last[h] = i;
		}
		int g;scanf("%d",&g);
		add_edge(i,T,g);
	}
	printf("%d\n",dinic());
	return 0;
}

总结:
要善于总结题目,而且要敢于想象。因为对于本题而言,显然第一反应都是以猪舍为点建边,而这次是以人为点建边。

[2007《最小割模型在信息学竞赛中的应用》] 网络战争

题目分析:
这是个显然的分数规划,那么就先套路搞一波:
我们枚举一个 \(\lambda\) 表示可能的答案,

\[\begin{aligned} \dfrac{\sum_{e \in C} w_e}{|C|} &< \lambda \\ \sum_{e \in C} w_e &< \lambda \times |C| \\ \sum_{e \in C} w_e - \lambda \times |C| &< 0 \\ \sum_{e \in C} (w_e - \lambda) &< 0 \\ \end{aligned} \]

对于最后一步的转化:因为我们边有 \(|C|\) 个,而 \(\lambda\) 也有 \(|C|\) 个,所以就可以对应相减。
显然 \(\lambda\) 满足二分单调性,所以就可以考虑二分。
上式的意思也就是我们选择一个边割集使得 \(\sum_{e \in C} (w_e - \lambda)\) 的最小值小于 \(0\)

注意:此处割集的概念为删去割集里的边 \(S\)\(T\) 不联通,而对于最小割里的割来说也就是它可以删除两个点集内部的边。

我们会显然发现一点:
如果 \(w_e - \lambda \le 0\),那么这些边选上之后一定是更优的。
而对于剩下的边我们只需要求一个最小割即为答案,因为删除这些边之后一定可以保证 \(S\)\(T\) 不联通,并且当我们删去最小割之外的边时因为 \(w_e - \lambda > 0\) 所以一定会使得答案变劣。

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const double eps = 1e-6;
const double INF = 1e9+5;
const int N = 1e5+5;
const int M = 1e6+5;
struct edge{
	int nxt,to;
	double val;
	edge(){}
	edge(int _nxt,int _to,double _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M],a[M];
int cnt = 1,s,t,n,m,dis[N],head[N],cur[N];
void add_edge(int from,int to,double val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,val);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,-1,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;q.push(s);dis[s] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == - 1 && e[i].val > eps){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	}
	return dis[t] != -1;
}
double dfs(int now,double limit){
	if(now == t)	return limit;
	double flow = 0;
	for(int i = cur[now]; i && flow - limit < -eps; i = e[i].nxt){
		int to = e[i].to;
		if(dis[to] == dis[now] + 1 && e[i].val > eps){
			double h = dfs(to,min(limit - flow,e[i].val));
			if(h < eps)	dis[to] = INF;
			e[i].val -= h;e[i^1].val+=h;flow+=h;
		}
	}
	return flow;
}
double dinic(){
	double ans = 0,flow;
	while(bfs()){
		while(1){
			flow = dfs(s,INF);
			if(flow < eps)	break;
			ans += flow;
		}
	}
	return ans;
}
bool check(double lamda){
	double sum = 0;
	for(int i=1; i<=m; i++){
		if(a[i].val - lamda <= 0)
			sum += (a[i].val - lamda);
		else
			add_edge(a[i].nxt,a[i].to,a[i].val - lamda);
	}
	sum += dinic();
	return sum < -eps;
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	for(int i=1; i<=m; i++){
		scanf("%lld%lld%lf",&a[i].nxt,&a[i].to,&a[i].val);
	}
	double l = 1,r = 10000000;
	while(r - l > eps){
		double mid = (l + r)/2;
		if(check(mid)){  //最后的和比 mid 小 
			r = mid;
		}
		else	l = mid;
	}
	printf("%.2f\n",r);
	return 0;
}

总结:
对于分数规划就是二分一个答案,然后将求解的式子进行转化,最后转化成一个可以维护的形式。
也要注意各种算法之间的相结合,这里就使用了贪心的策略和最小割模型和分数规划。

[2007《最小割模型在信息学竞赛中的应用》] 最优标号

题目分析:
显然看到异或我们就想到按位进行处理,因为各个数位之间相互独立。
假设我们现在处理到了第 \(k\) 位,我们会发现如果我们将所有点的标号分为 \(1\)\(0\) 两个集合,那么只有两个集合之间的边会对答案造成贡献,为了使得和最小显然就是使得之间的边数最小,发现和最小割很像。
我们设一个虚拟的源汇 \(S\)\(0\) 集合中,\(T\)\(1\) 集合中,假设我们原来的点 \(i\) 的标号固定为 \(0\),那么就连边 \(S\)\(i\),流量为 \(+\infty\),因为这样一定可以保证我们的最小割中割的边一定不是这一条,也就是 \(i\) 一定与 \(S\) 属于一个集合。
对于原图上的边正常建就好了,流量为 \(1\)

代码:

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5+5;
const int M = 2e6+5;
const int INF = 1e9+5;
struct edge{
	int nxt,to,val;
	edge(){}
	edge(int _nxt,int _to,int _val){
		nxt = _nxt,to = _to,val = _val;
	}
}e[M],a[M];
int n,m,k,cnt,s,t,head[N],cur[N],dis[N],p[N],u[N];
void add_edge(int from,int to,int val){
	e[++cnt] = edge(head[from],to,val);
	head[from] = cnt;
	e[++cnt] = edge(head[to],from,val);
	head[to] = cnt;
}
bool bfs(){
	memset(dis,-1,sizeof(dis));memcpy(cur,head,sizeof(head));
	queue<int> q;q.push(s);dis[s] = 1;
	while(!q.empty()){
		int now = q.front();q.pop();
		for(int i = head[now]; i; i = e[i].nxt){
			int to = e[i].to;
			if(dis[to] == - 1 && e[i].val){
				dis[to] = dis[now] + 1;
				q.push(to);
			}
		}
	}
	return dis[t] != -1;
}
int dfs(int now,int limit){
	if(now == t)	return limit;
	int flow = 0;
	for(int i = cur[now]; i && flow < limit; i = e[i].nxt){
		cur[now] = i;
		int to = e[i].to;
		if(dis[to] == dis[now] + 1 && e[i].val){
			int h = dfs(to,min(limit - flow,e[i].val));
			if(!h)	dis[to] = INF;
			e[i].val -= h;e[i^1].val += h;flow += h;
		}
	}	
	return flow;
}
int dinic(){
	int ans = 0,flow;
	while(bfs()){
		while(flow = dfs(s,INF)){
			ans += flow;
		}
	}
	return ans;
}
int get_ans(int pos){
	memset(head,0,sizeof(head));cnt=1;
	for(int i=1; i<=m; i++)	add_edge(a[i].nxt,a[i].to,1);
	for(int i=1; i<=k; i++){
		if((p[i]>>pos) & 1)	add_edge(s,u[i],INF);
		else	add_edge(u[i],t,INF);
	}
	return dinic();
}
signed main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%lld%lld",&n,&m);
	s = n + 1,t = s + 1;
	for(int i=1; i<=m; i++)	scanf("%lld%lld",&a[i].nxt,&a[i].to);
	scanf("%lld",&k);
	for(int i=1; i<=k; i++)	scanf("%lld%lld",&u[i],&p[i]);
	int ans = 0;
	for(int i=0; i<=30; i++)	ans += (1<<i) * get_ans(i);
	printf("%lld\n",ans);
	return 0;
}

总结:
异或的经典套路:按位处理。
将所有的点按照某个特点分为两个集合,然后在这两个集合上进行处理,也算是一个经典的模型。

posted @ 2022-08-13 13:56  linyihdfj  阅读(34)  评论(0编辑  收藏  举报