海亮 7.18 杂题选讲

海亮 7.18 杂题

P1989 无向图三元环计数

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

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

\(upd\):有向无环图证明:

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

\(out_1 < out_2 < out_3 < out_1\) (\(out_i\) 表示 \(i\) 点的出度)

显然不会存在这种情况。

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

那么该图就是一个\(DAG\).

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

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

那么考虑对于每一条边 \(<u \rightarrow v>\),它对复杂度造成的贡献是 \(out_v\),因此总复杂度即为 \(\sum_{i = 1}^m out_{v_i}\),其中 \(v_i\) 是第 \(i\) 条边指向的点,\(out_v\) 是点 \(v\) 的出度。

考虑分情况讨论。

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

因此所有节点的出度均为 \(O(\sqrt m)\),总复杂度 \(\sum_{i = 1}^m out_{v_i} = O(m \sqrt m)\)

#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 提高组] 魔法值

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

\[f_{x,i}=f_{v_1,i-1} \oplus f_{v_2,i-1} \oplus \cdots \oplus f_{v_k,i-1} \]

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

\(f_{x,i}=f_{1,i-1}\times e_{1,x}\oplus f_{2,i-1}\times e_{2,x} \oplus \cdots \oplus f_{n,i-1}\times e_{n,x}\)

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

\(c_{i,j}=a_{i,1} \times b_{1,j}+a_{i,2} \times b_{2,j}+\cdots+a_{i,n} \times b_{n,j}=\sum\limits_{k=1}^{n}a_{i,k}\times b_{k,j}\)

那么我们如果将\(f_{i,j}\)的定义调换一下 那么就有:

\[f_{i,x}=f_{i-1,1}\times e_{1,x}\oplus f_{i-1,2}\times e_{2,x} \oplus \cdots \oplus f_{i-1,n}\times e_{n,x} \]

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

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

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

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

具体证明可以见这里

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

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

对于多组询问 我们预处理\(2^0\)\(2^{31}\)的矩阵 进行二进制拆分即可

代码一:调换了\(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 入门组] 魔法

设置\(f_{k,i,j}\)为用了\(k\)次更改 \(i\)\(j\)的最小值

转移方程:\(f_{k, i, j} = \min_{t \in [1, n]} f_{k - 1, i, t} + f_{1, t, j}\) 可以看出这东西可以矩阵乘法 只不过运算符需要重载成\(min\)

可以\(floyd\)处理\(f_0\)矩阵 再枚举所有组边预处理出\(f_1\)矩阵

然后将初始状态\(f_0\)乘上\(f_1\)矩阵\(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(B*n)\)

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

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

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

我们可以按照叶子节点在前的\(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 @ 2023-07-18 20:38  Echo_Long  阅读(6)  评论(0编辑  收藏  举报