海亮 7.18 杂题选讲

海亮 7.18 杂题

P1989 无向图三元环计数

Subtask 1 有三种做法,分别是枚举三个点 O(n3),枚举两条邻边 O(m2),枚举一个点及其对边 O(nm)。这里不再赘述。

我们考虑给所有的边一个方向。具体的,如果一条边两个端点的度数不一样,则由度数较小的点连向度数较大的点,否则由编号较小的点连向编号较大的点。不难发现这样的图是有向无环的。注意到原图中的三元环一定与对应有向图中所有形如 <uv>,<uw>,<vw> 的子图一一对应,我们只需要枚举 u 的出边,再枚举 v 的出边,然后检查 w 是不是 u 指向的点即可。

upd:有向无环图证明:

假设存在环,按照我们连的有向边,那么应该满足 :

out1<out2<out3<out1 (outi 表示 i 点的出度)

显然不会存在这种情况。

会不会存在这三个点的出度相同的情况呢?也不会,因为我们会从编号小的连向编号大的点。

那么该图就是一个DAG.

下面证明这个算法的时间复杂度是 O(mm)

首先我们可以在枚举 u 的出边时给其出点打上 u 的时间戳,这样在枚举 v 的出边时即可 O(1) 去判断 w 是不是 u 的出点。

那么考虑对于每一条边 <uv>,它对复杂度造成的贡献是 outv,因此总复杂度即为 i=1moutvi,其中 vi 是第 i 条边指向的点,outv 是点 v 的出度。

考虑分情况讨论。

  1. v 在原图(无向图)上的度数不大于 m 时,由于新图每个节点的出度不可能大于原图的度数,所以 outv=O(m)
  2. v 在原图上的度数大于 m 时,注意到它只能向原图中度数不小于它的点连边,又因为原图中所有的点的度数和为 O(m),所以原图中度数大于 m 的点只有 O(m) 个。因此 v 的出边只有 O(m) 条,也即 outv=O(m)

因此所有节点的出度均为 O(m),总复杂度 i=1moutvi=O(mm)

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e6 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , x[N] , y[N] , dag[N] , ans , vis[N];

int head[N] , cnt;
struct DQY { int to , nxt; } e[N];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= m ; i ++ ) x[i] = read() , y[i] = read() , ++dag[x[i]] , ++dag[y[i]];		
	for ( int i = 1 ; i <= m ; i ++ )
	{
		int u = x[i] , v = y[i];
		if ( dag[u] > dag[v] ) swap ( u , v );
		else if ( dag[u] == dag[v] && u > v ) swap ( u , v );
		add ( u , v );
	}
	for ( int u = 1 ; u <= n ; u ++ )
	{
		for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt ) vis[v] = u; 
		for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
			for ( int j = head[v] ; j ; j = e[j].nxt )
				if ( vis[e[j].to] == u ) ans ++;
	}
	cout << ans << endl;
	return 0;
}


P6569 [NOI Online #3 提高组] 魔法值

首先 我们可以看到魔法值的定义:

fx,i=fv1,i1fv2,i1fvk,i1

那有了邻接矩阵 e 之后 有边为1 没边为 0 我们就可以将答案作如下转化:

fx,i=f1,i1×e1,xf2,i1×e2,xfn,i1×en,x

可以类比这个柿子(矩阵乘法):

ci,j=ai,1×b1,j+ai,2×b2,j++ai,n×bn,j=k=1nai,k×bk,j

那么我们如果将fi,j的定义调换一下 那么就有:

fi,x=fi1,1×e1,xfi1,2×e2,xfi1,n×en,x

这样就符合矩阵乘法的形式了

(实际上可以不调换定义 只调换乘法的顺序即可 具体见代码二)

那么我们将矩阵乘法改为"异或矩阵乘法"即可

它对于非01矩阵是不满足结合律的 但是01矩阵满足

具体证明可以见这里

实际上我们可以将异或看成mod2意义下的加法 那么每一个位置就是定长路径计数的答案mod2的结果

对于初始值 是将所有值(第0天的值) 输入f[1][k](1kn)

对于多组询问 我们预处理20231的矩阵 进行二进制拆分即可

代码一:调换了f的定义

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
const int N = 1e2 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , q;

struct DQY 
{
	int mat[N][N] , n , m;
	void clear () { memset ( mat , 0 , sizeof mat ); }
	friend DQY operator * ( DQY a , DQY b )
	{
		DQY res; res.clear();
		res.n = a.n , res.m = b.m;
		for ( int k = 1 ; k <= a.m ; k ++ ) 
			for ( int i = 1 ; i <= a.n ; i ++ )
				for ( int j = 1; j <= b.m ; j ++ )
					res.mat[i][j] ^= a.mat[i][k] * b.mat[k][j];
		return res;
	}
}base , k[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read() , q = read();
	base.clear() , base.n = 1 , base.m = n;
	k[0].clear() , k[0].n = n , k[0].m = n;
	for ( int i = 1 ; i <= n ; i ++ ) base.mat[1][i] = read();
	for ( int i = 1 , u , v ; i <= m ; i ++ ) u = read() , v = read() , k[0].mat[u][v] = 1 , k[0].mat[v][u] = 1;
	for ( int i = 1 ; i <= 31 ; i ++ ) k[i] = k[i-1] * k[i-1];
//	for ( int i = 1 ; i <= n ; i ++ , cout.put('\n') , cout.put(endl) )
//		for ( int j = 1 ; j <= k[i].n ; j ++ , cout.put(endl) )
//			for ( int l = 1 ; l <= k[i].m ; l ++ ) 
//				cout << k[i].mat[j][l] << ' ';
	for ( int i = 1 ; i <= q ; i ++ )
	{
		int now = read();
		DQY ans = base;
		for ( int j = 0 ; j <= 31 ; j ++ )
			if ( now & ( 1ll << j ) ) ans = ans * k[j];
		cout << ans.mat[1][1] << endl;
	}
	return 0;
}

代码二:只需要改一下乘法的顺序即可(输入和乘法的时候略微做了修改)

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
const int N = 1e2 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , q;

struct DQY 
{
	int mat[N][N] , n , m;
	void clear () { memset ( mat , 0 , sizeof mat ); }
	friend DQY operator * ( DQY a , DQY b )
	{
		DQY res; res.clear();
		res.n = a.n , res.m = b.m;
		for ( int k = 1 ; k <= a.m ; k ++ ) 
			for ( int i = 1 ; i <= a.n ; i ++ )
				for ( int j = 1; j <= b.m ; j ++ )
					res.mat[i][j] ^= a.mat[i][k] * b.mat[k][j];
		return res;
	}
}base , k[N];

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read() , q = read();
	base.clear() , base.n = n , base.m = 1;
	k[0].clear() , k[0].n = n , k[0].m = n;
	for ( int i = 1 ; i <= n ; i ++ ) base.mat[i][1] = read();
	for ( int i = 1 , u , v ; i <= m ; i ++ ) u = read() , v = read() , k[0].mat[u][v] = 1 , k[0].mat[v][u] = 1;
	for ( int i = 1 ; i <= 31 ; i ++ ) k[i] = k[i-1] * k[i-1];
//	for ( int i = 1 ; i <= n ; i ++ , cout.put('\n') , cout.put(endl) )
//		for ( int j = 1 ; j <= k[i].n ; j ++ , cout.put(endl) )
//			for ( int l = 1 ; l <= k[i].m ; l ++ ) 
//				cout << k[i].mat[j][l] << ' ';
	for ( int i = 1 ; i <= q ; i ++ )
	{
		int now = read();
		DQY ans = base;
		for ( int j = 0 ; j <= 31 ; j ++ )
			if ( now & ( 1ll << j ) ) ans = k[j] * ans;
		cout << ans.mat[1][1] << endl;
	}
	return 0;
}

P6190 [NOI Online #1 入门组] 魔法

设置fk,i,j为用了k次更改 ij的最小值

转移方程:fk,i,j=mint[1,n]fk1,i,t+f1,t,j 可以看出这东西可以矩阵乘法 只不过运算符需要重载成min

可以floyd处理f0矩阵 再枚举所有组边预处理出f1矩阵

然后将初始状态f0乘上f1矩阵k

ans.mat[1][n]即为答案

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define int long long
const int N = 1e2 + 5;
const int M = 2500 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , K , u[M] , v[M] , w[M] , edge[M][M];

struct DQY 
{
	int mat[N][N];
	DQY() { memset ( mat , 0x3f , sizeof mat ); }
	friend DQY operator * ( DQY a , DQY b )
	{
		DQY ret;
		for ( int k = 1 ; k <= n ; k ++ )
			for ( int i = 1 ; i <= n ; i ++ )
				for ( int j = 1 ; j <= n ; j ++ )
					ret.mat[i][j] = min ( ret.mat[i][j] , a.mat[i][k] + b.mat[k][j] );
		return ret;
	}
}f,res;

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read() , K = read();
	for ( int i = 1 ; i <= m ; i ++ ) u[i] = read() , v[i] = read() , w[i] = read() , f.mat[u[i]][v[i]] = w[i] , edge[u[i]][v[i]] = w[i];
	for ( int i = 1 ; i <= n ; i ++ ) f.mat[i][i] = 0;
	for ( int k = 1 ; k <= n ; k ++ ) for ( int i = 1 ; i <= n ; i ++ ) for ( int j = 1 ; j <= n ; j ++ ) f.mat[i][j] = min ( f.mat[i][j] , f.mat[i][k] + f.mat[k][j] );
	if ( !K ) { cout << f.mat[1][n] << endl; return 0; }
	for ( int i = 1 ; i <= n ; i ++ ) for ( int j = 1 ; j <= n ; j ++ ) res.mat[i][j] = f.mat[i][j];
	for ( int k = 1 ; k <= m ; k ++ ) for ( int i = 1 ; i <= n ; i ++ ) for ( int j = 1 ; j <= n ; j ++ ) f.mat[i][j] = min ( f.mat[i][j] , res.mat[i][u[k]] + res.mat[v[k]][j] - edge[u[k]][v[k]] );
	while ( K )
	{
		if ( K & 1 ) res = res * f;
		f = f * f , K >>= 1;
	}
	cout << res.mat[1][n] << endl;
	return 0;
}


Piotr's Ants

是下面一道题的前置题捏(虽然不在题单里)

显然有一个结论:无论如何动 从左到右的编号序列是不变的

所以我们记录一个rk[i]数组表示第i个输入的点的排名位置 输出的时候直接用rk[i]在最后的状态中定位即可

注意两组数据之间要有一个空行

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e4 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int L , timer , n , now[N] , rk[N];

char ch;
struct DQY { int pos , fx , id; } st[N] , ed[N];

string s[3] = { "L" , "Turning" , "R" };

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int T = read();
	for ( int cases = 1 ; cases <= T ; cases ++ )
	{
		L = read() , timer = read() , n = read();
		for ( int i = 1 , pos , fx ; i <= n ; i ++ )
		{
			pos = read() , cin >> ch;
			fx = ( ch == 'L' ) ? -1 : 1;
			st[i] = { pos , fx , i };
			ed[i] = { pos + fx * timer , fx , i };
		}
		sort ( st + 1 , st + n + 1 , [](const DQY &a , const DQY &b) { return a.pos < b.pos; } );
		sort ( ed + 1 , ed + n + 1 , [](const DQY &a , const DQY &b) { return a.pos < b.pos; } );
		for ( int i = 1 ; i <= n ; i ++ ) rk[st[i].id] = i;
		for ( int i = 1 ; i < n ; i ++ ) if ( ed[i].pos == ed[i+1].pos ) ed[i].fx = ed[i+1].fx = 0;
		cout << "Case #" << cases << ':' << endl;
		for ( int i = 1 ; i <= n ; i ++ )
			if ( ed[rk[i]].pos < 0 || ed[rk[i]].pos > L ) cout << "Fell off" << endl;
			else cout << ed[rk[i]].pos << ' ' << s[ed[rk[i]].fx+1] << endl;
		cout << endl;
	}
	return 0;
}

P5835 [USACO19DEC] Meetings S

首先考虑一个结论 无论奶牛怎么走 整个序列从左到右的体重序列一定是不变的

因为我们两只奶牛相交的时候相当于是奶牛不变 交换体重

设之前轻的在左 重的在右 那么我们交换体重之后 轻的变成重的 继续向右走(这时它已经在右面了) 反之亦然

所以显然体重序列不变

考虑到时间具有单调性 即时间越多 奶牛一定越能走到头 所以可以二分答案

其他套路跟上一题基本类似

rk表示输入顺序为i的节点在原来序列的位置 rev表示排名为i的数字在原序列的编号 排序即可

特别注意输入的时候wei数组不能放在结构体中 否则会导致排序后找不到对应的重量

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
const int N = 1e5 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int L , timer , n , now[N] , rk[N] , rev[N] , sum , ans , wei[N];
//这里的wei需要单独开一个数组 不能放在结构体中 因为我们在用rev索引的时候 放在结构体中的wei已经打乱顺序了() 
char ch;
struct DQY { int pos , fx , id; friend bool operator < ( const DQY a , const DQY b ) { return a.pos == b.pos ? a.fx < b.fx : a.pos < b.pos; } } a[N] , temp[N] , f[N];

int check ( int x )
{
	int res = 0;
	for ( int i = 1 ; i <= n ; i ++ ) temp[i] = a[i] , temp[i].pos += temp[i].fx * x;
	sort ( temp + 1 , temp + n + 1 );
	for ( int i = 1 ; i <= n ; i ++ ) if ( temp[i].pos >= L || temp[i].pos <= 0 ) res += wei[rev[i]];
	return res * 2 >= sum;
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , L = read();
	for ( int i = 1 ; i <= n ; i ++ ) wei[i] = read() , a[i].pos = read() , a[i].fx = read() , a[i].id = i , sum += wei[i];
	sort ( a + 1 , a + n + 1 );
	for ( int i = 1 ; i <= n ; i ++ ) rk[a[i].id] = i , rev[i] = a[i].id;
	int l = 0 , r = 1e9;
	while ( l <= r )
	{
		if ( check(mid) ) r = mid - 1;
		else l = mid + 1;
	}
	for ( int i = 1 ; i <= n ; i ++ ) temp[i] = a[i] , temp[i].pos += temp[i].fx * l;
	sort ( temp + 1 , temp + n + 1 );
	for ( int i = 1 ; i <= n ; i ++ ) if ( temp[i].fx == 1 ) ans += i - rk[temp[i].id];
	cout << ans << endl; 
	return 0;
}


Tests for problem D

构造题 我们先dfs到叶子节点 为一个父亲节点的所有子区间确定好左坐标 再为父亲确定左坐标

再按照逆序提取出来子区间 倒序赋值右区间 这样可以保证一个父亲节点的所有子区间是有包含关系的

先赋值l[u]的原因是保证所有子节点的区间都可以和l这个端点相交

最后不要忘了处理根节点的右端点

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
const int N = 1e6 + 5;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , l[N] , r[N] , idx;

int head[N] , cnt;
struct DQY { int to , nxt; } e[N]; 
void add ( int u , int v ) { e[++cnt] = { v , head[u] } , head[u] = cnt; }

int dfs ( int u , int fa )
{
	vector<int> vec;
	for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt ) if ( v != fa ) vec.push_back(v) , dfs ( v , u );
	l[u] = ++idx;
	while ( !vec.empty() ) r[vec.back()] = ++idx , vec.pop_back();
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	dfs ( 1 , 0 );
	r[1] = ++idx;
	for ( int i = 1 ; i <= n ; i ++ ) cout << l[i] << ' ' << r[i] << endl;
	return 0;
}

You Are Given a Tree

观察到k有单调性 考虑根号分治 设置阈值B

对于小于B的点直接暴力做O(n) 这部分的复杂度是O(Bn)

否则直接二分答案 二分答案为i的位置的右位置

将这些位置都赋值为这个答案 二分O(logn) dfsO(n) 答案数不超过n/B

那么可以证明整体复杂度在B=nlogn的时候最优 为O(2nnlogn)

我们可以按照叶子节点在前的dfn序进行dp 每次保证取出的节点u的子树都是处理完了的

只要能组合成大于k的链 那么贪心选取即可

那么我们可以向父亲上推 进行dp

以为是被卡常了 原来是循环的递增条件写错了()

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
const int N = 1e5 + 5;
int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}
 
int n , B , ans[N] , fa[N] , f[N] , dfn[N] , timer;
 
vector<int> e[N];
 
void dfs ( int u , int ff )
{
	fa[u] = ff;
	for ( auto v : e[u] )
		if ( v != ff ) dfs ( v , u );
	dfn[++timer] = u;
}
 
int solve ( int k )
{
	int res = 0;
	for ( int i = 1 ; i <= n ; i ++ ) f[i] = 1;
	for ( int i = 1 ; i <= n ; i ++ )
	{
		int x = dfn[i];
		if ( fa[x] && f[fa[x]] != -1 && f[x] != -1 )
		{
			if ( f[x] + f[fa[x]] >= k ) res ++ , f[fa[x]] = -1;
			else f[fa[x]] = max ( f[fa[x]] , f[x] + 1 );
		}
	}
	return res;
}
 
signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , B = sqrt ( n * log2(n) );
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , e[u].push_back(v) , e[v].push_back(u);
	dfs ( 1 , 0 ) , ans[1] = n;
	for ( int i = 2 ; i <= B ; i ++ ) ans[i] = solve(i);
	for ( int i = B + 1 , l , r , res ; i <= n ; i = r + 1 )
	{
		l = i , r = n , res = solve(l);
		while ( l <= r )
		{
			if ( solve(mid) == res ) l = mid + 1;
			else r = mid - 1;
		}
		for ( int j = i ; j <= r ; j ++ ) ans[j] = res;
	}
	for ( int i = 1 ; i <= n ; i ++ ) cout << ans[i] << endl;
	return 0;
}

posted @   Echo_Long  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示