The 13th Chinese Northeast Collegiate Programming Contest
题解
C:
算出斜率和截距sort一下就行,每次要减去的贡献是平行的直线数-重叠的直线数。
#include
using namespace std;
using ll = long long;
using ld = long double;
const int N = 1e5+7;
const ld eps = 1e-13;
int sgn(ld x) {
if(fabs(x)<eps) return 0;
return x<0?-1:1;
}
struct Line {
ll y, x;
ld b;
bool operator < (const Line&rhs) const {
return y*rhs.x<x*rhs.y||(y*rhs.x==x*rhs.y&&sgn(b-rhs.b)<0);
}
bool operator == (const Line&rhs) const {
return y*rhs.x==x*rhs.y&&sgn(b-rhs.b)==0;
}
}ln[N];
// (x-x1)/(x2-x1)=(y-y1)/(y2-y1)
int main() {
// freopen("out.txt", "w", stdout);
int T;
scanf("%d", &T);
while(T--) {
int n;
scanf("%d", &n);
for(int i=0; i<n; ++i) {
ll xa, ya, xb, yb;
scanf("%I64d%I64d%I64d%I64d", &xa, &ya, &xb, &yb);
ll x=xb-xa;
ll y=yb-ya;
ld b;
if(x==0) {
b = xa;
y=1;
} else if(y==0) {
x = 1;
b = ya;
} else {
ll g=__gcd(abs(x), abs(y));
x/=g;
y/=g;
if(x<0) {
y=-y;
x=-x;
}
b=-(ld)xa/(xb-xa)*(yb-ya)+ya;
// cout << "ggg: " << x << " " << y << " " << g << " " << b << endl;
}
ln[i] = {y, x, b};
}
ll res = 0;
sort(ln, ln+n);
int r1=0, r2=0;
//printf("sgn: %d\n", sgn(ln[1].b-ln[0].b));
for(int l=0; l<n; ++l) {
// printf("line %d: %I64d %I64d %.6f\n", l, ln[l].x, ln[l].y, (double)ln[l].b);
while(r1<n&&ln[r1]==ln[l]) ++r1;
while(r2<n&&(ln[r2].y*ln[l].x==ln[r2].x*ln[l].y)) ++r2;
res += r2-r1;
// printf("l=%d r1=%d r2=%d\n", l, r1, r2);
}
ll ans = 1LL*n*(n-1)/2-res;
printf("%I64d\n", ans);
}
return 0;
}
D:
首先学一下虚树。
虚树指的是一种压缩树的方法。假设我们只在意某些关键点。我们可以在虚树中保存原树中的关键点和关键点之间的LCA,虚树中的边是原树中链的压缩。
虚树一般在询问链的时候才能使用,且需要保证边权/点权可以压缩。假设树的大小为 n n n ,关键点的数量为 k k k ,那么构建虚树的复杂度是 O ( k l o g n ) O(klogn) O(klogn) 。因此,如果有多次询问,每次的关键点很少,那么用虚树非常合适。
虚树的建树方法是使用单调栈维护一条链。单调栈中的所有点在一条链上,且dfs序递增。当有一个新关键点加入时,如果新关键点与栈顶的lca就是栈顶,那么说明它们在一条链上,无需特别处理。如果不是栈顶,那么依次将栈顶弹出,直到栈顶下方的节点的dfs序小于等于LCA的dfs序,此时我们将栈顶弹出,与LCA连边,然后将LCA压入栈中。
在实现过程中,每次连边可以用倍增的方法将链上的信息进行压缩存到边上或者点上。当每次询问时,需要将邻接表清空,如果依次遍历清空邻接表非常耗时。我们可以选择在点入栈的时候清空,因为入栈前是不可能有连边的,这样降低了复杂度。
此外,当结束时,需要将栈中的所有点弹出来,相邻两个点之间进行连边。
当虚树建立好后,在虚树上进行dp或修改时跟原树基本一样。
因为这题所有点权初始都相同(为0),然后这题的询问量很少,因此可以离线读所有操作点,然后对这些操作点建立一个虚树,对于加/减/异或三种修改操作,直接暴力的修改点权就行。对于6,7两种最值查询操作,直接取点权的最小值和最大值即可。对于4,5求和和异或和,需要维护虚树中每个点之上的这条链中被压缩的点的个数,为了方便,这题将所有LCA的父亲节点也建到了虚树中,因此所有LCA之上的被压缩个数就是1 。对于所有操作,暴力剖出链上的所有点(包括LCA),然后遍历一遍进行查询或修改即可。
复杂度 O ( n + m 2 ) O(n+m^2) O(n+m2) 。
#include
using namespace std;
const int N = 5e5+7;
const int INF = 1e9+7;
typedef long long ll;
struct graph {
int head[N], nxt[N*2], to[N*2], sz;
void init() { memset(head, -1, sizeof(head)); sz=0; }
graph() { init(); }
void push(int u, int v) {
// printf("addedge: %d %d\n", u, v);
nxt[sz]=head[u];
to[sz] = v;
head[u]=sz++;
}
} g;
struct Query {
int op, u, v, k;
}q[2007];
int pre[N][20], dep[N], id[N], dfn, sz[N], fa[N], w[N];
int sta[N], top;
void init(int a, int fa) {
pre[a][0] = fa;
dep[a] = dep[fa]+1;
id[a]=++dfn;
for(int i=1; i<20; ++i) {
pre[a][i] = pre[pre[a][i-1]][i-1];
}
for(int i=g.head[a]; i!=-1; i=g.nxt[i]) {
if(g.to[i]!=fa) {
init(g.to[i], a);
}
}
}
int lca(int a, int b) {
if(dep[a]>dep[b]) swap(a, b);
for(int i=19; i>=0; --i) {
if(dep[pre[b][i]]>=dep[a]) {
b = pre[b][i];
}
}
if(a==b) return a;
for(int i=19; i>=0; --i) {
if(pre[a][i]!=pre[b][i]) {
a = pre[a][i];
b = pre[b][i];
}
}
return pre[a][0];
}
vector<int> path;
void get_path(int x, int y) {
path.clear();
while(x!=y) {
if(dep[x]<dep[y]) swap(x, y);
path.push_back(x);
x = fa[x];
}
path.push_back(x);
}
void dfs(int x) {
for(int i = g.head[x]; i!=-1; i=g.nxt[i]) {
fa[g.to[i]] = x;
dfs(g.to[i]);
}
}
bool cmp(int a, int b) {
return id[a] < id[b];
}
vector<int> pts;
int main() {
int T;
scanf("%d", &T);
while(T--) {
int n, m;
scanf("%d%d", &n, &m);
g.init();
dfn = 0;
memset(w, 0, sizeof(w));
for(int i=1; i<n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
g.push(u, v);
g.push(v, u);
}
init(1, 0);
pts.clear();
pts.push_back(1);
for(int i=0; i<m; ++i) {
scanf("%d%d%d", &q[i].op, &q[i].u, &q[i].v);
if(q[i].op<=3||q[i].op==7) scanf("%d", &q[i].k);
pts.push_back(q[i].u); pts.push_back(q[i].v);
int l = lca(q[i].u, q[i].v);
if(pre[l][0]) pts.push_back(pre[l][0]);
}
sort(pts.begin(), pts.end(), cmp); //dfs序排序所有关键点。
pts.erase(unique(pts.begin(), pts.end()), pts.end());
sta[top=1]=1; //栈内其实维护着一条到关键点的链上的关键点/LCA等,它们的dfs序递增
g.sz = 0;
g.head[1] = -1;
for(int i=0, l; i<pts.size(); ++i) {
if(pts[i]!=1) {
l = lca(sta[top], pts[i]);
if(l!=sta[top]) { //lca没在栈中,弹栈元素出来
while(id[l]<id[sta[top-1]]) { //找到dfs序小于等于LCA的次栈顶。
lca(sta[top-1], sta[top]); //连边时计算在原树中这条链的最小值
g.push(sta[top-1], sta[top]); //出栈时连边
sz[sta[top]] = dep[sta[top]]-dep[sta[top-1]];
top--;
}
if(id[l]>id[sta[top-1]]) { //如果次栈顶和lca不是相同点
g.head[l] = -1; //新入栈的LCA,初始化一下。
lca(l, sta[top]); //连接LCA和旧栈顶
g.push(l, sta[top]);
sz[sta[top]] = dep[sta[top]] - dep[l];
sta[top] = l; //让LCA作为新栈顶
} else { //如果次栈顶和LCA是相同点,那么次栈顶变为新栈顶
lca(l, sta[top]);
g.push(l, sta[top]);
sz[sta[top]] = dep[sta[top]] - dep[l];
--top;
}
}
g.head[pts[i]] = -1; //新入栈的关键点,初始化一下。
sta[++top] = pts[i];
}
}
for(int i=1; i<top; ++i) {
lca(sta[i], sta[i+1]);
g.push(sta[i], sta[i+1]);
sz[sta[i+1]] = dep[sta[i+1]] - dep[sta[i]];
}
sz[1] = 1;
// for(int v : pts) {
// printf("sz: %d %d\n", v, sz[v]);
// }
dfs(1);
for(int i=0; i<m; ++i) {
int op = q[i].op;
int u = q[i].u;
int v = q[i].v;
int k = q[i].k;
get_path(u, v);
if(op==1) {
for(int v : path) w[v] += k;
} else if(op==2) {
for(int v : path) w[v] ^= k;
} else if(op==3) {
for(int v : path) {
if(w[v]>=k) w[v]-=k;
}
} else if(op==4) {
ll ans = 0;
for(int v : path) ans += 1LL*sz[v]*w[v];
printf("%I64d\n", ans);
} else if(op==5) {
int ans = 0;
for(int v : path) ans ^= (sz[v]%2)*w[v];
printf("%d\n", ans);
} else if(op==6) {
int mx = -INF, mn=INF;
for(int v : path) {
mx = max(mx, w[v]);
mn = min(mn, w[v]);
}
printf("%d\n", mx-mn);
} else if(op==7) {
int ans = INF;
for(int v : path) ans = min(ans, abs(w[v]-k));
printf("%d\n", ans);
}
}
}
}
F:
可以维护 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示第 i i i 个结点,当前是第 j j j 个人时是A赢( − 1 -1 −1 ),平局( 0 0 0 ),还是B赢( 1 1 1)。那么对于无法在继续移动的点,可以很容易算出其答案。因为一个状态的答案取决于它所能转移到的所有状态的答案。因此当一个状态答案更新完后,看一下指向它的状态是否所有出边状态的答案都更新好了,如果更新好了,就将它入队(队列中存放的是已经确定答案的状态),当它出队时,根据所有它能转移到的状态答案更新它自己的答案。如果一个状态的答案是A赢,且指向它的一个状态的 d p [ i ] [ j ] dp[i][j] dp[i][j] 的 j j j 属于A队,那么显然 d p [ i ] [ j ] dp[i][j] dp[i][j] 也是A赢,这时说明它的状态确定了,也需要入队。最后剩下的未入队的状态答案不定,因此打印D。
#include
using namespace std;
const int N = 2e5+7;
vector<int> adj[N], inv[N];
int d[N][6], tem[6], w[6], dp[N][6], vis[N][6];
struct Node {
int x, y;
};
char s[10];
int main() {
int T;
scanf("%d", &T);
while(T--) {
int n, m;
scanf("%d%d", &n, &m);
for(int i=1; i<=n; ++i) {
adj[i].clear();
inv[i].clear();
}
memset(dp, 0, sizeof(dp));
memset(d, 0, sizeof(d));
memset(vis, 0, sizeof(vis));
for(int i=0; i<m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
adj[u].push_back(v);
inv[v].push_back(u);
for(int j=0; j<6; ++j) d[u][j]++;
}
scanf("%s", s);
for(int i=0; i<6; ++i) tem[i] = s[i]-'A';
scanf("%s", s);
for(int i=0; i<6; ++i) w[i]=tem[i]^(s[i]-'0');
queue<Node> q;
for(int i=1; i<=n; ++i) {
for(int j=0; j<6; ++j) {
if(!d[i][j]) q.push({i, j}), vis[i][j]=true;
}
}
while(!q.empty()) {
Node nd = q.front(); q.pop();
int x=nd.x, y=nd.y;
// printf("state: %d %d\n", x, y);
int ny = (y+1)%6; // 查看所指向边的状态
// 确定当前状态的值
if(adj[x].size()==0) dp[x][y] = tem[y]==0?1:-1;
else if(w[y]==0) {
dp[x][y] = 1;
for(int u : adj[x]) {
if(dp[u][ny]==-1) {
dp[x][y] = -1;
break;
} else if(dp[u][ny]==0) {
dp[x][y] = 0;
}
}
} else if(w[y]==1) {
dp[x][y] = -1;
for(int u : adj[x]) {
if(dp[u][ny]==1) {
dp[x][y] = 1;
break;
} else if(dp[u][ny]==0) {
dp[x][y] = 0;
}
}
}
ny = (y+5)%6; //查看是否有新的可以确定状态的点。
for(int u : inv[x]) {
if(vis[u][ny]) continue;
d[u][ny]--;
if(!d[u][ny]) q.push({u, ny}), vis[u][ny]=true;
else if(w[ny]==0&&dp[x][y]==-1) q.push({u, ny}), vis[u][ny]=true;
else if(w[ny]==1&&dp[x][y]==1) q.push({u, ny}), vis[u][ny]=true;
}
}
for(int i=1; i<=n; ++i) {
if(dp[i][0]==-1) putchar('A');
else if(dp[i][0]==1) putchar('B');
else putchar('D');
}
putchar('\n');
}
}