海亮 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\) 的出度。
考虑分情况讨论。
- 当 \(v\) 在原图(无向图)上的度数不大于 \(\sqrt m\) 时,由于新图每个节点的出度不可能大于原图的度数,所以 \(out_v = O(\sqrt m)\)。
- 当 \(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 提高组] 魔法值
首先 我们可以看到魔法值的定义:
那有了邻接矩阵 \(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}\)的定义调换一下 那么就有:
这样就符合矩阵乘法的形式了
(实际上可以不调换定义 只调换乘法的顺序即可 具体见代码二)
那么我们将矩阵乘法改为"异或矩阵乘法"即可
它对于非\(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;
}