【集训】最小生成树!
未学习:Kruskal 重构树,可持久化并查集,Boruvka
最小生成树
P2330 [SCOI2005] 繁忙的都市
最小生成树
最小瓶颈树
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define ROF(i, a, b) for (int i = (a); i >= (b); --i)
#define DEBUG(x) cerr << #x << " = " << x << endl
#define ll long long
typedef pair <int, int> PII;
typedef unsigned int uint;
typedef unsigned long long ull;
#define i128 __int128
#define fi first
#define se second
mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
#define ClockA clock_t start, end; start = clock()
#define ClockB end = clock(); cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
//#define int long long
inline int rd(){
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
#define rd rd()
void wt(int x){
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
wt(x / 10);
putchar(x % 10 + '0');
return;
}
void wt(char x){
putchar(x);
}
void wt(int x, char k){
wt(x),putchar(k);
}
namespace Star_F{
const int N = 100005;
int fa[N], sum;
int cnt;
struct node{
int u, v, w;
friend bool operator<(node a,node b){
return a.w < b.w;
}
}a[N];
int find(int x){
return fa[x] == x ? fa[x] : fa[x] = find(fa[x]);
}
void Main(){
int n = rd, m = rd;
FOR(i, 1, m)
a[i].u = rd, a[i].v = rd, a[i].w = rd;
FOR(i, 1, n) fa[i] = i;
sort(a + 1, a + m + 1);
FOR(i,1,m){
int fu = find(a[i].u);
int fv = find(a[i].v);
if(fu==fv) continue;
fa[fu] = fv;
sum = a[i].w;
cnt++;
if(cnt==n-1)
break;
}
cout << n - 1 << " " << sum << endl;
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ClockA;
int T=1;
// T=rd;
while(T--) Star_F::Main();
// ClockB;
return 0;
}
P4047 [JSOI2010] 部落划分
假如
这个过程其实也就是 Kruskal 的过程,直接跑 Kruskal 到剩
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1050,M=1e6+100;
int n,k;
int fa[N];
struct node {
int x,y;
double v;
} p[M];
bool cmp(node a,node b) {
return a.v<b.v;
}
int find(int x) {
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
void merge(int x,int y) {
if(find(x)==find(y)) return;
fa[fa[x]]=fa[y];
return;
}
double x[N],y[N];
int main() {
cin>>n>>k;
for(int i=1; i<=n; i++) fa[i]=i;
for(int i=1;i<=n;i++)
cin>>x[i]>>y[i];
int tot=0;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++)
p[++tot]=(node){i,j,sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]))};
sort(p+1,p+1+tot,cmp);
int lft=n;
double mx=0;
for(int i=1; i<=tot; i++){
int a=p[i].x,b=p[i].y;
if(find(a)==find(b)) continue;
merge(a,b);
lft--;
mx=p[i].v;
if(lft<k) break;
}
printf("%.2lf\n",mx);
return 0;
}
P2573 [SCOI2012] 滑雪
我们会发现不管我们往下怎么连边,都不会对高度比它高的产生影响,所以我们在给点排序应该以节点高度为第一关键字,距生成树的距离为第二关键字。而最小生成树的算法都是将能接到树上的节点全都接上,所以我们只需要在建树树统计一下就可以算出最多能到景点数量了。
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define ROF(i, a, b) for (int i = (a); i >= (b); --i)
#define DEBUG(x) cerr << #x << " = " << x << endl
#define ll long long
typedef pair <int, int> PII;
typedef unsigned int uint;
typedef unsigned long long ull;
#define i128 __int128
#define fi first
#define se second
mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
#define ClockA clock_t start, end; start = clock()
#define ClockB end = clock(); cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
//#define int long long
inline int rd(){
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
#define rd rd()
void wt(int x){
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
wt(x / 10);
putchar(x % 10 + '0');
return;
}
void wt(char x){
putchar(x);
}
void wt(int x, char k){
wt(x),putchar(k);
}
namespace Star_F{
const int N = 200005, M = 2000005;
int n, m, H[N];
ll cnt, ans, dis[N], vis[N];
int h[N], e[M], ne[M], w[M], idx;
void add(int u,int v,int W){
e[++idx] = v, ne[idx] = h[u], w[idx] = W, h[u] = idx;
}
struct node{
int h, dis, id;
bool friend operator<(node a,node b){
if(a.h!=b.h)
return a.h < b.h;
return a.dis > b.dis;
}
};
priority_queue<node> q;
void prim(){
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
q.push({h[1], 0, 1});
while(!q.empty()){
int u = q.top().id;
q.pop();
if(vis[u])
continue;
vis[u] = 1;
cnt++, ans += dis[u];
for (int i = h[u]; i;i=ne[i]){
int v = e[i];
if(vis[v])
continue;
if(dis[v]>w[i])
dis[v] = w[i], q.push({H[v], dis[v], v});
}
}
}
void Main(){
cin >> n >> m;
for (int i = 1; i <= n;i++)
cin >> H[i];
for (int i = 1; i <= m;i++){
int u, v, w;
cin >> u >> v >> w;
if(H[u]>=H[v])
add(u, v, w);
if(H[v]>=H[u])
add(v, u, w);
}
prim();
cout << cnt << " "<< ans << endl;
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ClockA;
int T=1;
// T=rd;
while(T--) Star_F::Main();
// ClockB;
return 0;
}
P8207 [THUPC 2022 初赛] 最小公倍树
Kruskal 优化建图
观察到如果直接建图的时间复杂度是
首先,我们有
这实际上启发了我们从
而我们可以枚举因子,因子的范围是
根据调和级数,这个东西的边数是
时间复杂度是
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define ROF(i, a, b) for (int i = (a); i >= (b); --i)
#define DEBUG(x) cerr << #x << " = " << x << endl
#define ll long long
typedef pair <int, int> PII;
typedef unsigned int uint;
typedef unsigned long long ull;
#define i128 __int128
#define fi first
#define se second
mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
#define ClockA clock_t start, end; start = clock()
#define ClockB end = clock(); cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
//#define int long long
inline int rd(){
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
#define rd rd()
void wt(int x){
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
wt(x / 10);
putchar(x % 10 + '0');
return;
}
void wt(char x){
putchar(x);
}
void wt(int x, char k){
wt(x),putchar(k);
}
namespace Star_F{
const int N = 1000005;
int l, r, f[N], buc[N];
ll gcd(ll a, ll b){ return !b ? a : gcd(b, a % b); }
ll lcm(ll a, ll b){ return a / gcd(a, b) * b;}
int find(int x){
return f[x] == x ? x : f[x] = find(f[x]);
}
struct node{
int l, r;
ll w;
bool friend operator<(node a,node b){
return a.w < b.w;
}
};
vector<node> G;
ll ans, cnt;
void Kruskal(){
sort(G.begin(), G.end());
for(auto x:G){
ll u = x.l, v = x.r, w = x.w;
if(find(u)!=find(v)){
f[max(find(u), find(v))] = min(find(u), find(v));
ans += w;
}
}
}
void Main(){
cin >> l >> r;
for (int i = l; i <= r;i++)
f[i] = i, buc[i] = 1;
for (int i = 2; i <= r;i++){
int tmp = 0;
for (int j = i; j <= r;j+=i){
if(buc[j]&&!tmp)
tmp = j;
if(buc[j])
G.push_back({tmp, j, lcm(tmp, j)});
}
if(i>=l)
G.push_back({tmp, l, lcm(tmp, l)});
}
Kruskal();
cout << ans << endl;
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ClockA;
int T=1;
// T=rd;
while(T--) Star_F::Main();
// ClockB;
return 0;
}
P3623 [APIO2008] 免费道路
优先加
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define ROF(i, a, b) for (int i = (a); i >= (b); --i)
#define DEBUG(x) cerr << #x << " = " << x << endl
#define ll long long
typedef pair <int, int> PII;
typedef unsigned int uint;
typedef unsigned long long ull;
#define i128 __int128
#define fi first
#define se second
mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
#define ClockA clock_t start, end; start = clock()
#define ClockB end = clock(); cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
//#define int long long
inline int rd(){
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
#define rd rd()
void wt(int x){
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
wt(x / 10);
putchar(x % 10 + '0');
return;
}
void wt(char x){
putchar(x);
}
void wt(int x, char k){
wt(x),putchar(k);
}
namespace Star_F{
const int N = 200005, M = 100005;
int n, m, k, fa[N], tot, cnt;
struct edge{
int u, v, w;
} e[M], ans[M];
bool cmp1(edge a,edge b){
return a.w > b.w;
}
bool cmp2(edge a,edge b){
return a.w < b.w;
}
int find(int x){
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
bool merge(int x,int y){
x = find(x),y = find(y);
if(x==y)
return false;
fa[x] = y;
return true;
}
void init(){
cnt = tot = 0;
for (int i = 1; i <= n;i++)
fa[i] = i;
}
void check(){
int tmp = find(1);
for (int i = 2; i <= n;i++){
int x = find(i);
if(x!=tmp){
cout << "no solution" << endl;
exit(0);
}
tmp = x;
}
}
void Main(){
cin >> n >> m >> k;
for (int i = 1; i <= m;i++)
cin >> e[i].u >> e[i].v >> e[i].w;
init();
sort(e + 1, e + m + 1, cmp1);
for (int i = 1; i <= m;i++)
if(merge(e[i].u,e[i].v)&&e[i].w==0)
tot++, e[i].w = -1;
if(tot>k){
cout << "no solution" << endl;
exit(0);
}
check();
init();
sort(e + 1, e + m + 1, cmp2);
for (int i = 1; i <= m;i++){
int f1 = find(e[i].u), f2 = find(e[i].v);
if(f1==f2)
continue;
if(e[i].w==1||tot<k){
ans[++cnt] = e[i];
fa[f1] = f2;
if(e[i].w<1){
tot++;
e[i].w = 0;
}
}
}
if(tot<k){
cout << "no solution" << endl;
exit(0);
}
check();
for (int i = 1; i <= cnt;i++){
if(ans[i].w==-1)
ans[i].w = 0;
cout << ans[i].u << " " << ans[i].v << " " << ans[i].w << endl;
}
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ClockA;
int T=1;
// T=rd;
while(T--) Star_F::Main();
// ClockB;
return 0;
}
P4768 [NOI2018] 归程
Kruskal 重构树,可持久化并查集
CF1120D Power Tree
我们可以先找出这棵树的 dfs 序。dfs 序的特点是,相邻叶节点的 dfs 序总是连续的。
那对于每个点,我们就可以求出,它所能控制的叶节点对应的 dfn 序的一个区间。
这样就转化为一个区间问题了:
给定若干段区间 [l, r],每个区间有一个代价 a_i(对应原图的点权),还有一段 [1, n] 的序列 a。
控制一个区间,表示可以对区间里的数任意加减。
求出最少代价以及方案,不管 a_i 是多少,总能将 a 的所有数变为 0。
区间多次修改,单次查询,很容易想到差分。
每次相当于在差分数组
并且显然有
换句话说,我们所能更改的数是
那么接下来就非常简单了:
然后跑一遍 kruskal 即可。事实上转化为区间这一步只是推导过程,实际代码只需要在求出区间
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); ++i)
#define ROF(i, a, b) for (int i = (a); i >= (b); --i)
#define DEBUG(x) cerr << #x << " = " << x << endl
#define ll long long
typedef pair <int, int> PII;
typedef unsigned int uint;
typedef unsigned long long ull;
#define i128 __int128
#define fi first
#define se second
mt19937 rnd(chrono::system_clock::now().time_since_epoch().count());
#define ClockA clock_t start, end; start = clock()
#define ClockB end = clock(); cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
//#define int long long
inline int rd(){
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9'){
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
x = (x << 3) + (x << 1) + ch - '0', ch = getchar();
return x * f;
}
#define rd rd()
void wt(int x){
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
wt(x / 10);
putchar(x % 10 + '0');
return;
}
void wt(char x){
putchar(x);
}
void wt(int x, char k){
wt(x),putchar(k);
}
namespace Star_F{
const int N = 200005;
int n, m, a[N];
int fa[N];
int h[N], idx;
struct node{
int u, v, w, pos;
} e[N];
void add1(int u,int v,int pos){
e[++m].u = u, e[m].v = v;
e[m].w = a[pos], e[m].pos = pos;
}
bool cmp(node a,node b){
return a.w < b.w;
}
void init(){
for (int i = 1; i <= n + 1;i++)
fa[i] = i;
}
int find(int x){
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
struct edge{
int u, nxt, w;
} edge[N << 1];
void add2(int u,int v){
edge[++idx].u = v, edge[idx].nxt = h[u], h[u] = idx;
}
int dfn[N], dfnr[N];
void dfs(int u,int fa){
bool flag = 1;
dfn[u] = 0x3f3f3f3f;
for (int i = h[u]; i;i=edge[i].nxt){
int v = edge[i].u;
if(v==fa)
continue;
flag = 0, dfs(v, u);
dfn[u] = min(dfn[u], dfn[v]), dfnr[u] = max(dfnr[u], dfnr[v]);
}
if(flag)
dfn[u] = dfnr[u] = ++idx;
add1(dfn[u], dfnr[u] + 1, u);
}
int tot;
bool ans[N];
ll Kruskal(){
init();
sort(e + 1, e + m + 1, cmp);
ll sum = 0;
for (int l = 1; l <= n;){
int r;
for (r = l; r + 1 <= n && e[r].w == e[r + 1].w;r++);
for (int i = l; i <= r;i++)
if(find(e[i].u)!=find(e[i].v))
ans[e[i].pos] = 1, tot++;
for (int i = l; i <= r;i++){
int x = find(e[i].u), y = find(e[i].v);
if(x==y)
continue;
fa[x] = y, sum += e[i].w;
}
l = r + 1;
}
return sum;
}
void Main(){
cin >> n;
for (int i = 1; i <= n;i++)
cin >> a[i];
for (int i = 1; i < n;i++){
int u, v;
cin >> u >> v;
add2(u, v), add2(v, u);
}
idx = 0, dfs(1, 0);
cout << Kruskal() << " " << tot << endl;
for (int i = 1; i <= n;i++)
if(ans[i])
cout << i << " ";
}
}
signed main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
ClockA;
int T=1;
// T=rd;
while(T--) Star_F::Main();
// ClockB;
return 0;
}
CF1550F Jumping Around
考虑对于每个结点
对于两个结点
根据最小生成树的性质,实际上建出最小生成树后,
边数为
这个东西就是维护每个联通块,然后对于每次迭代,对于每个联通块找到它连出去的最小的边,然后全部找完之后合并。因为每次联通块的次数会至少减半,所以迭代的次数是
虽然边数仍然是
那么利用一个集合维护所有的点,对于每个联通块,先删去这个联通块中的所有点,然后再遍历所有点,用
然后再按照上面所说的在这个树上进行 DFS 一次即可。
P4180 [BJWC2010] 严格次小生成树
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100010;
const ll INF = 1e18;
int n, m,d[N]; // n表示节点数,m表示边数,d[]是节点的深度
bool vis[N]; // vis[]标记节点是否已访问
ll f[N][25],g1[N][25],g2[N][25],mst, ans = INF; // f[]是LCA的父节点,g1[]和g2[]存储两条最大边
struct Node {
ll to,cost; // 表示边的终点和权重
};
vector<Node> v[N]; // 邻接表存储图
// 深度优先遍历,构建树的深度和LCA所需要的数据
void dfs(const int x) {
vis[x] = true;
for (int i = 0; i < v[x].size(); i++) {
int y = v[x][i].to;
if (vis[y]) continue;
d[y] = d[x] + 1;
f[y][0] = x; // 记录y的父节点
g1[y][0] = v[x][i].cost; // 记录边的权重
g2[y][0] = -INF; // 初始化第二大权重
dfs(y);
}
}
// 预处理LCA的父节点及最大最小边的相关信息
inline void prework() {
for (int i = 1; i <= 20; i++) // 最大深度是log(n),因此最多需要20层
for (int j = 1; j <= n; j++) {
f[j][i] = f[f[j][i - 1]][i - 1]; // 设置父节点
g1[j][i] = max(g1[j][i - 1], g1[f[j][i - 1]][i - 1]); // 记录最大边
g2[j][i] = max(g2[j][i - 1], g2[f[j][i - 1]][i - 1]); // 记录第二大边
if (g1[j][i - 1] > g1[f[j][i - 1]][i - 1]) g2[j][i] = max(g2[j][i], g1[f[j][i - 1]][i - 1]); // 若第一大边改变,第二大边可能更新
else if (g1[j][i - 1] < g1[f[j][i - 1]][i - 1]) g2[j][i] = max(g2[j][i], g1[j][i - 1]); // 若第二大边改变,也要更新
}
}
// 计算两节点的LCA,并考虑次小生成树
inline void LCA(int x, int y, const ll w) {
ll zui = -INF, ci = -INF; // zui表示最大边权,ci表示第二大边权
if (d[x] > d[y]) swap(x, y); // 保证x的深度小于等于y
for (int i = 20; i >= 0; i--) // 将y上升到与x相同的深度
if (d[f[y][i]] >= d[x]) {
zui = max(zui, g1[y][i]);
ci = max(ci, g2[y][i]);
y = f[y][i];
}
if (x == y) { // 如果x和y是同一个节点
if (zui != w) ans = min(ans, mst - zui + w); // 如果最大边不是w,更新答案
else if (ci != w && ci > 0) ans = min(ans, mst - ci + w); // 如果第二大边不是w,更新答案
return;
}
for (int i = 20; i >= 0; i--) // 如果x和y不同,找公共祖先
if (f[x][i] != f[y][i]) {
zui = max(zui, max(g1[x][i], g1[y][i])); // 更新最大边权
ci = max(ci, max(g2[x][i], g2[y][i])); // 更新第二大边权
x = f[x][i];
y = f[y][i];
}
zui = max(zui, max(g1[x][0], g1[y][0])); // 更新最终的最大边
if (g1[x][0] != zui) ci = max(ci, g1[x][0]); // 如果最大边不等于zui,更新第二大边
if (g2[y][0] != zui) ci = max(ci, g2[y][0]);
if (zui != w) ans = min(ans, mst - zui + w); // 如果最大边不是w,更新答案
else if (ci != w && ci > 0) ans = min(ans, mst - ci + w); // 如果第二大边不是w,更新答案
}
struct Edge {
int from, to;
ll cost;
bool is_tree; // 标记边是否属于生成树
} edge[N * 3];
bool operator < (const Edge x, const Edge y) {
return x.cost < y.cost; // 根据边的权重排序
}
int fa[N];
// 并查集查找操作
inline int find(const int x) {
if (fa[x] == x) return x;
else return fa[x] = find(fa[x]);
}
// Kruskal算法求最小生成树
inline void Kruskal() {
sort(edge, edge + m); // 根据边的权重排序
for (int i = 1; i <= n; i++) fa[i] = i; // 初始化并查集
for (int i = 0; i < m; i++) {
int x = edge[i].from;
int y = edge[i].to;
ll z = edge[i].cost;
int a = find(x), b = find(y); // 查找x和y的根节点
if (a == b) continue; // 如果x和y已经连通,跳过
fa[find(x)] = y; // 合并两个连通分量
mst += z; // 更新最小生成树的权重
edge[i].is_tree = true; // 标记这条边属于生成树
v[x].push_back((Node) {y, z}); // 将这条边加入邻接表
v[y].push_back((Node) {x, z}); // 将这条边加入邻接表
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cin >> n >> m; // 输入节点数和边数
for (int i = 0, x, y; i < m; i++) {
ll z;
cin >> x >> y >> z; // 输入边的两个端点和权重
if (x == y) continue; // 如果是自环,跳过
edge[i].from = x;
edge[i].to = y;
edge[i].cost = z;
}
Kruskal(); // 求最小生成树
d[1] = 1;
dfs(1); // 深度优先遍历,构建树的深度信息和LCA数据
prework(); // 预处理LCA数据
for (int i = 0; i < m; i++) // 遍历所有非生成树边
if (!edge[i].is_tree)
LCA(edge[i].from, edge[i].to, edge[i].cost); // 对每条非生成树边调用LCA
cout << ans << "\n"; // 输出次小生成树的权重
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库