Dlaczego C ++ STL nie zapewnia żadnych kontenerów „drzewiastych”?


373

Dlaczego C ++ STL nie zapewnia żadnych kontenerów „drzewiastych” i czego najlepiej użyć zamiast tego?

Chcę przechowywać hierarchię obiektów jako drzewo, zamiast używać drzewa jako ulepszenia wydajności ...


7
Potrzebuję drzewa do przechowywania reprezentacji hierarchii.
Roddy,

20
Jestem z facetem, który w głosowaniu głosował na „prawidłowe” odpowiedzi, które wydają się być; „Drzewa są bezużyteczne”. Ważne są niejasne zastosowania drzew.
Joe Soul-bringer

Myślę, że powód jest trywialny - nikt jeszcze nie zaimplementował go w standardowej bibliotece. To tak, jakby standardowa biblioteka nie miała std::unordered_mapi std::unordered_setdo niedawna. A wcześniej nie było żadnych kontenerów STL w standardowej bibliotece.
dok.

1
Moje przemyślenia (chociaż nigdy nie przeczytałem odpowiedniego standardu, stąd jest to komentarz, a nie odpowiedź) są takie, że STL nie dba o określone struktury danych, dba o specyfikacje dotyczące złożoności i obsługiwanych operacji. Tak więc zastosowana podstawowa struktura może się różnić w zależności od implementacji i / lub architektury docelowej, pod warunkiem, że jest zgodna ze specyfikacjami. Jestem całkiem pewien std::mapi std::setużyję drzewa w każdej implementacji, ale nie muszą tego robić, jeśli jakaś struktura inna niż drzewna spełnia również specyfikacje.
Mark K Cowan

Odpowiedzi:


182

Istnieją dwa powody, dla których warto użyć drzewa:

Chcesz odwzorować problem za pomocą struktury drzewiastej:
Do tego mamy bibliotekę grafów pomocniczych

Lub chcesz kontener, który ma cechy dostępu podobne do drzewa. Do tego mamy

Zasadniczo cechy tych dwóch kontenerów są takie, że praktycznie muszą zostać zaimplementowane przy użyciu drzew (choć tak naprawdę nie jest to wymagane).

Zobacz także to pytanie: Implementacja drzewa C.


64
Istnieje wiele, wiele powodów, aby używać drzewa, nawet jeśli są one najczęściej. Najczęściej! Równe wszystkim.
Joe Soul-bringer

3
Trzecim głównym powodem, dla którego chce się drzewa, jest zawsze posortowana lista z szybkim wstawianiem / usuwaniem, ale do tego istnieje std: multiset.
VoidStar

1
@Durga: Nie jestem pewien, jak głębokość jest istotna, gdy używasz mapy jako posortowanego pojemnika. Mapa gwarantuje wstawianie / usuwanie / wyszukiwanie log (n) (i zawierające elementy w posortowanej kolejności). To wszystko służy do mapowania i jest implementowane (zwykle) jako drzewo czerwone / czarne. Czerwone / czarne drzewo zapewnia równowagę drzewa. Głębokość drzewa jest więc bezpośrednio związana z liczbą elementów w drzewie.
Martin York,

14
Nie zgadzam się z tą odpowiedzią, zarówno w 2008 r., Jak i obecnie. Biblioteka standardowa nie ma „wzmocnienia”, a dostępność czegoś wzmocnionego nie powinna być (i nie była) powodem, aby nie dostosować go do standardu. Ponadto BGL ma charakter ogólny i jest wystarczająco zaangażowany, aby zasłużyć na wyspecjalizowane klasy drzew niezależne od niego. Również fakt, że std :: map i std :: set wymagają drzewa, jest IMO, kolejnym argumentem przemawiającym za posiadaniem stl::red_black_treeitd. Wreszcie drzewa std::mapi std::setsą zrównoważone, a std::treebyć może nie.
einpoklum

1
@einpoklum: „dostępność czegoś wzmocnionego nie powinna być powodem, aby nie dostosować go do standardu” - biorąc pod uwagę, że jednym z celów wzmocnienia jest działanie jako sprawdzian dla użytecznych bibliotek przed włączeniem do standardu, mogę tylko powiedz „absolutnie!”.
Martin Bonner wspiera Monikę

94

Prawdopodobnie z tego samego powodu, dla którego nie ma kontenera drzewa doładowania. Istnieje wiele sposobów na wdrożenie takiego kontenera i nie ma dobrego sposobu na zaspokojenie wszystkich, którzy by z niego korzystali.

Niektóre kwestie do rozważenia:

  • Czy liczba dzieci w węźle jest stała czy zmienna?
  • Ile narzutów na węzeł? - tzn. czy potrzebujesz wskaźników nadrzędnych, wskaźników rodzeństwa itp.
  • Jakie algorytmy podać? - różne iteratory, algorytmy wyszukiwania itp.

Ostatecznie problemem jest to, że pojemnik na drzewo, który byłby wystarczająco użyteczny dla wszystkich, byłby zbyt ciężki, aby zadowolić większość osób, które z niego korzystają. Jeśli szukasz czegoś potężnego, Boost Graph Library jest w zasadzie nadzbiorem tego, do czego biblioteka drzewek mogłaby zostać użyta.

Oto kilka innych ogólnych implementacji drzewa:


5
„... nie ma dobrego sposobu na zaspokojenie wszystkich ...” Z wyjątkiem tego, że ponieważ stl :: map, stl :: multimap i stl :: set są oparte na rb_tree stl, powinno spełniać tyle samo przypadków, co te podstawowe typy .
Catskul

44
Biorąc pod uwagę, że nie ma sposobu na odzyskanie dzieci węzła std::map, nie nazwałbym tych kontenerów drzewnych. Są to pojemniki asocjacyjne, które są zwykle implementowane jako drzewa. Duża różnica.
Mooing Duck

2
Zgadzam się z Mooing Duck, jak byś wdrożył szerokie pierwsze wyszukiwanie na std :: map? Będzie to strasznie drogie
Marco A.

1
Zacząłem używać drzewa Kaspera Peetersa. Hh, jednak po przejrzeniu licencji na GPLv3 lub jakąkolwiek inną wersję GPL, zanieczyściłoby to nasze oprogramowanie komercyjne. Polecam przyjrzeć się treetree podanemu w komentarzu @hplbsh, jeśli potrzebujesz struktury do celów komercyjnych.
Jake88

3
Specyficzne wymagania dotyczące drzew w odniesieniu do odmian są argumentem za tym, aby mieć różne rodzaje drzew, a nie mieć ich wcale.
André

50

Filozofia STL polega na tym, że wybierasz kontener na podstawie gwarancji, a nie na podstawie sposobu implementacji kontenera. Na przykład wybór kontenera może być oparty na potrzebie szybkiego wyszukiwania. Dla ciebie wszystko jedno: kontener może być zaimplementowany jako lista jednokierunkowa - dopóki wyszukiwanie jest bardzo szybkie, będziesz szczęśliwy. To dlatego, że i tak nie dotykasz elementów wewnętrznych, używasz iteratorów lub funkcji członkowskich w celu uzyskania dostępu. Twój kod nie jest związany z tym, jak kontener jest zaimplementowany, ale z tym, jak szybki jest, czy ma ustaloną i zdefiniowaną kolejność, czy jest efektywny pod względem miejsca i tak dalej.


12
Nie sądzę, żeby mówił o implementacjach kontenerów, mówił o samym kontenerze drzewa.
Mooing Duck 30.09.13

3
@MooingDuck Myślę, że Wilhelmtell oznacza, że ​​standardowa biblioteka C ++ nie definiuje kontenerów na podstawie ich podstawowej struktury danych; definiuje tylko kontenery na podstawie ich interfejsu i obserwowalnych cech, takich jak wydajność asymptotyczna. Kiedy się nad tym zastanowić, drzewo tak naprawdę wcale nie jest kontenerem (tak jak je znamy). Oni nawet nie mają aa prosto do przodu end()i begin()z którym można wykonać iterację wszystkich elementów, itp
Jordan Melo

7
@JordanMelo: Bzdury we wszystkich punktach. To rzecz, która zawiera przedmioty. Bardzo trywialne jest zaprojektowanie go tak, aby zawierał iteratory begin () i end () oraz dwukierunkowe iteratory. Każdy pojemnik ma inną charakterystykę. Przydałoby się, gdyby można było dodatkowo mieć cechy drzewa. Powinno być całkiem łatwe.
Mooing Duck

Dlatego chce się mieć kontener zapewniający szybkie wyszukiwanie węzłów podrzędnych i nadrzędnych oraz rozsądne wymagania dotyczące pamięci.
dok.

@JordanMelo: Z tej perspektywy również adaptery, takie jak kolejki, stosy lub kolejki priorytetowe, nie powinny należeć do STL (nie mają również begin()i end()). I pamiętaj, że kolejka priorytetowa jest zwykle stertą, która przynajmniej teoretycznie jest drzewem (chociaż rzeczywiste implementacje). Więc nawet jeśli zaimplementowałeś drzewo jako adapter za pomocą innej podstawowej struktury danych, kwalifikowałoby się do włączenia do STL.
andreee

48

„Chcę przechowywać hierarchię obiektów jako drzewo”

C ++ 11 przyszedł i zniknął i nadal nie widzieli potrzeby zapewnienia std::tree, chociaż pomysł się pojawił (patrz tutaj ). Być może powodem, dla którego tego nie dodali, jest to, że łatwo jest zbudować własny na istniejących kontenerach. Na przykład...

template< typename T >
struct tree_node
   {
   T t;
   std::vector<tree_node> children;
   };

Proste przejście wymagałoby rekurencji ...

template< typename T >
void tree_node<T>::walk_depth_first() const
   {
   cout<<t;
   for ( auto & n: children ) n.walk_depth_first();
   }

Jeśli chcesz utrzymać hierarchię i chcesz, aby działała z algorytmami STL , sprawy mogą się skomplikować. Możesz zbudować własne iteratory i osiągnąć pewną zgodność, jednak wiele algorytmów po prostu nie ma sensu dla hierarchii (na przykład wszystko, co zmienia kolejność zakresu). Nawet zdefiniowanie zakresu w hierarchii może być bałaganem.


2
Jeśli projekt umożliwia sortowanie elementów potomnych tree_node, wówczas użycie std :: set <> zamiast std :: vector <>, a następnie dodanie operatora <() do obiektu tree_node znacznie poprawi się wydajność „wyszukiwania” obiektu typu „T”.
J Jorgenson

4
Okazuje się, że byli leniwi i faktycznie zrobili twój pierwszy przykład Niezdefiniowane zachowanie.
user541686,

2
@ Mehrdad: W końcu postanowiłem zapytać o szczegóły twojego komentarza tutaj .
nobar

many of the algorithms simply don't make any sense for a hierarchy. Kwestia interpretacji. Wyobraź sobie strukturę użytkowników z przepełnieniem stosu i co roku chcesz, aby ci z większą ilością punktów reputacji kierowali tymi z niższymi punktami reputacji. Zapewniając w ten sposób iterator BFS i odpowiednie porównanie, każdego roku po prostu uruchamiasz std::sort(tree.begin(), tree.end()).
dokument

Z tego samego powodu można łatwo zbudować drzewo asocjacyjne (w celu modelowania nieustrukturyzowanych rekordów klucz-wartość, na przykład JSON), zastępując vectorje mapw powyższym przykładzie. Aby uzyskać pełną obsługę struktury podobnej do JSON, można użyć variantdo zdefiniowania węzłów.
nobar

43

Jeśli szukasz implementacji drzewa RB, plik stl_tree.h może być odpowiedni również dla Ciebie.


14
O dziwo, to jedyna odpowiedź, która faktycznie odpowiada na pierwotne pytanie.
Catskul

12
Biorąc pod uwagę, że chce on „Heiarchy”, wydaje się bezpiecznie założyć, że cokolwiek z „balansowaniem” jest złą odpowiedzią.
Mooing Duck 30.09.13

11
„To jest wewnętrzny plik nagłówka, dołączany przez inne nagłówki biblioteki. Nie powinieneś próbować używać go bezpośrednio.”
Dan

3
@ Dan: Kopiowanie go nie oznacza używania go bezpośrednio.
einpoklum

12

std :: map jest oparty na czerwonym, czarnym drzewie . Możesz także użyć innych pojemników, aby pomóc Ci wdrożyć własne typy drzew.


13
Zwykle używa czerwono-czarnych drzew (nie jest to wymagane).
Martin York,

1
GCC używa drzewa do implementacji mapy. Czy ktoś chce spojrzeć na ich katalog dołączania VC, aby zobaczyć, z czego korzysta Microsoft?
JJ

// Czerwono-czarna klasa drzewa, zaprojektowana do użycia przy implementacji kontenerów asocjacyjnych STL // (set, multiset, map i multimap). Wziąłem to z mojego pliku stl_tree.h.
JJ

@ JJ Przynajmniej w Studio 2010 używa ordered red-black tree of {key, mapped} values, unique keysklasy wewnętrznej , zdefiniowanej w <xtree>. Nie masz w tej chwili dostępu do bardziej nowoczesnej wersji.
Justin Time - Przywróć Monikę

8

W pewnym sensie, std :: map jest drzewem (wymagane jest, aby mieć takie same parametry wydajności jak zrównoważone drzewo binarne), ale nie ujawnia innych funkcjonalności drzewa. Prawdopodobne uzasadnienie nieuwzględnienia prawdziwej struktury danych drzewa było prawdopodobnie tylko kwestią niewłączenia wszystkiego do STL. STL może być postrzegany jako środowisko do implementacji własnych algorytmów i struktur danych.

Ogólnie rzecz biorąc, jeśli potrzebujesz podstawowej funkcji biblioteki, której nie ma w STL, poprawka polega na spojrzeniu na BOOST .

W przeciwnym razie istnieje grono z bibliotekami się tam , w zależności od potrzeb drzewa.


6

Wszystkie kontenery STL są reprezentowane zewnętrznie jako „sekwencje” z jednym mechanizmem iteracji. Drzewa nie podążają za tym idiomem.


7
Struktura danych drzewa może zapewniać przechodzenie przed, po zamówieniu lub po zamówieniu przez iteratory. W rzeczywistości to właśnie robi std :: map.
Andrew Tomazos,

3
Tak i nie ... to zależy od tego, co rozumiesz przez „drzewo”. std::mapjest wewnętrznie zaimplementowany jako btree, ale zewnętrznie pojawia się jako posortowana SEKWENCJA PAR. Biorąc pod uwagę dowolny element, możesz ogólnie zapytać, kto jest przed, a kto po nim. Ogólne struktury drzewiaste zawierające elementy, z których każdy zawiera inny, nie narzucają żadnego sortowania ani kierunku. Możesz zdefiniować iteratory, które chodzą po strukturze drzewa na wiele sposobów (ziemiste | głębokie pierwsze | ostatnie ...), ale gdy to zrobisz, std::treekontener musi zwrócić jeden z nich z beginfunkcji. I nie ma oczywistego powodu, aby zwrócić jedno lub drugie.
Emilio Garavaglia,

4
Std :: map jest ogólnie reprezentowane przez zrównoważone drzewo wyszukiwania binarnego, a nie B-drzewo. Ten sam argument, który podałeś, może odnosić się do std :: unordered_set, nie ma on naturalnego porządku, ale prezentacje rozpoczynają i kończą iteratory. Wymóg początku i końca polega tylko na tym, że iteruje wszystkie elementy w pewnej deterministycznej kolejności, a nie że musi być naturalny. preorder to doskonale poprawna kolejność iteracji dla drzewa.
Andrew Tomazos,

4
Implikacja twojej odpowiedzi jest taka, że ​​nie ma struktury danych drzewa stl n, ponieważ nie ma ona interfejsu „sekwencji”. To jest po prostu nieprawidłowe.
Andrew Tomazos,

3
@EmiloGaravaglia: Jak to udowodniono std::unordered_set, który nie ma „unikalnego sposobu” iteracji swoich członków (w rzeczywistości kolejność iteracji jest pseudolosowa i zdefiniowana implementacja), ale wciąż jest kontenerem STL - to nie zgadza się z twoim argumentem. Iteracja po każdym elemencie w kontenerze jest nadal przydatną operacją, nawet jeśli kolejność jest niezdefiniowana.
Andrew Tomazos,

4

Ponieważ STL nie jest biblioteką „wszystkiego”. Zawiera zasadniczo minimalne struktury potrzebne do budowania rzeczy.


13
Drzewa binarne są niezwykle podstawową funkcjonalnością, a właściwie bardziej podstawową niż inne kontenery, ponieważ typy takie jak std :: map, std :: multimap i stl :: set. Ponieważ te typy są oparte na nich, można oczekiwać, że typ bazowy zostanie ujawniony.
Catskul

2
Nie sądzę, że OP prosi o drzewo binarne , on prosi o drzewo do przechowywania hierarchii.
Kaczka Mooing

Ponadto dodanie „kontenera” drzewa do STL oznaczałoby dodanie wielu nowych koncepcji, na przykład nawigatora drzewa (uogólniający Iterator).
alfC

5
„Minimalne struktury do budowania rzeczy” to bardzo subiektywne stwierdzenie. Możesz budować rzeczy z surowymi koncepcjami C ++, więc sądzę, że prawdziwym minimum nie byłoby STL.
dok.


3

IMO, pominięcie. Ale myślę, że jest dobry powód, aby nie uwzględniać struktury drzewa w STL. W utrzymywaniu drzewa jest dużo logiki, którą najlepiej zapisywać jako funkcje składowe w TreeNodeobiekcie podstawowym . Kiedy TreeNodejest zawinięty w nagłówek STL, robi się coraz bardziej bałagan.

Na przykład:

template <typename T>
struct TreeNode
{
  T* DATA ; // data of type T to be stored at this TreeNode

  vector< TreeNode<T>* > children ;

  // insertion logic for if an insert is asked of me.
  // may append to children, or may pass off to one of the child nodes
  void insert( T* newData ) ;

} ;

template <typename T>
struct Tree
{
  TreeNode<T>* root;

  // TREE LEVEL functions
  void clear() { delete root ; root=0; }

  void insert( T* data ) { if(root)root->insert(data); } 
} ;

7
To wiele posiadania surowych wskaźników, które tam masz, z których wiele wcale nie musi być wskaźnikami.
Mooing Duck 30.09.13

Zaproponuj wycofanie tej odpowiedzi. Klasa TreeNode jest częścią implementacji drzewa.
einpoklum

3

Myślę, że istnieje kilka powodów, dla których nie ma drzew STL. Przede wszystkim Drzewa są formą rekurencyjnej struktury danych, która podobnie jak kontener (lista, wektor, zestaw), ma bardzo różną drobną strukturę, co utrudnia prawidłowe wybory. Są również bardzo łatwe do zbudowania w podstawowej formie za pomocą STL.

Skończone zrootowane drzewo może być traktowane jako pojemnik, który ma wartość lub ładunek, powiedzmy, przykład klasy A i ewentualnie pustej kolekcji ukorzenionych (pod) drzew; drzewa z pustą kolekcją poddrzewa są uważane za liście.

template<class A>
struct unordered_tree : std::set<unordered_tree>, A
{};

template<class A>
struct b_tree : std::vector<b_tree>, A
{};

template<class A>
struct planar_tree : std::list<planar_tree>, A
{};

Trzeba się trochę zastanowić nad projektem iteratora itp. Oraz nad tym, które operacje produktu i produktu towarzyszącego pozwalają definiować i być wydajne między drzewami - a oryginalny STL musi być dobrze napisany - aby pusty kontener, wektor lub lista była naprawdę pozbawiony ładunku w domyślnym przypadku.

Drzewa odgrywają istotną rolę w wielu strukturach matematycznych (zobacz klasyczne artykuły Butchera, Grossmana i Larsena; także w artykułach Connesa i Kriemera przykłady ich łączenia i sposobu ich wyliczania). Błędem jest myśleć, że ich rolą jest po prostu ułatwianie niektórych innych operacji. Ułatwiają one raczej te zadania ze względu na ich podstawową rolę jako struktury danych.

Jednak oprócz drzew istnieją również „współ-drzewa”; przede wszystkim drzewa mają tę właściwość, że usunięcie korzenia powoduje usunięcie wszystkiego.

Rozważ iteratory na drzewie, prawdopodobnie byłyby one realizowane jako zwykły stos iteratorów, do węzła i do jego elementu nadrzędnego ... aż do katalogu głównego.

template<class TREE>
struct node_iterator : std::stack<TREE::iterator>{
operator*() {return *back();}
...};

Możesz jednak mieć tyle, ile chcesz; wspólnie tworzą „drzewo”, ale tam, gdzie wszystkie strzały płyną w kierunku korzenia, to wspólne drzewo może być iterowane przez iteratory w kierunku trywialnego iteratora i korzenia; jednak nie można nawigować w poprzek lub w dół (inne iteratory nie są mu znane) ani też zespół iteratorów nie może zostać usunięty, chyba że śledzi wszystkie instancje.

Drzewa są niezwykle przydatne, mają dużą strukturę, co stanowi poważne wyzwanie, aby uzyskać zdecydowanie poprawne podejście. Moim zdaniem dlatego nie są one zaimplementowane w STL. Co więcej, w przeszłości widziałem ludzi religijnych i pomysł na typ kontenera zawierającego instancje własnego typu stanowił wyzwanie - ale muszą stawić temu czoła - to reprezentuje typ drzewa - jest to węzeł zawierający ewentualnie pusta kolekcja (mniejszych) drzew. Obecny język pozwala na to bez żadnych wątpliwości, zapewniając domyślny konstruktorcontainer<B> nie przydziela miejsca na stercie (lub gdziekolwiek indziej) na Bitp.

Z jednej strony byłbym zadowolony, gdyby to w dobrej formie znalazło się w normie.


0

Czytając tutaj odpowiedzi, najczęściej wymienianymi powodami są to, że nie można iterować przez drzewo lub drzewo nie przyjmuje podobnego interfejsu do innych kontenerów STL i nie można używać algorytmów STL o takiej strukturze drzewa.

Mając to na uwadze, próbowałem zaprojektować własną strukturę danych drzewa, która zapewni interfejs podobny do STL i będzie w jak największym stopniu możliwa do wykorzystania z istniejącymi algorytmami STL.

Mój pomysł polegał na tym, że drzewo musi być oparte na istniejących kontenerach STL i nie może ukrywać kontenera, aby można go było używać z algorytmami STL.

Inną ważną cechą, którą drzewo musi zapewnić, są iteratory ruchu.

Oto, co udało mi się wymyślić: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

A oto testy: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp


-9

Wszystkie kontenery STL mogą być używane z iteratorami. Nie możesz mieć iteratora na drzewie, ponieważ nie masz „jednej właściwej” drogi, aby przejść przez drzewo.


3
Ale możesz powiedzieć, że BFS lub DFS to właściwy sposób. Lub wesprzyj ich obu. Lub jakikolwiek inny, jaki możesz sobie wyobrazić. Po prostu powiedz użytkownikowi, co to jest.
tomas789,

2
w std :: map znajduje się iterator drzew.
Jai

1
Drzewo może zdefiniować własny niestandardowy typ iteratora, który przemierza wszystkie węzły w kolejności od jednego „skrajnego” do drugiego (tj. Dla dowolnego drzewa binarnego ze ścieżkami 0 i 1, może oferować iterator, który przechodzi od „wszystkich 0” do „wszystkich” 1S „i odwrotny iteracyjnej, która działa przeciwnie, na drzewo o głębokości 3 do węzła wyjściowego s, na przykład, może to iteracyjnego węzłach s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(” od lewej”na«skrajna»); może też użyć wzoru głębokość przejścia ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Przywróć Monikę

itp.) lub jakiś inny wzorzec, o ile iteruje po każdym węźle w taki sposób, że każdy z nich przechodzi tylko jeden raz.
Justin Time - Przywróć Monikę

1
@doc, bardzo dobry punkt. Wydaje mi się, że std::unordered_set„utworzono” sekwencję, ponieważ nie znamy lepszego sposobu iteracji elementów niż inny arbitralny sposób (wewnętrznie podany przez funkcję skrótu). Wydaje mi się, że jest to odwrotny przypadek drzewa: powtórzenie unordered_setjest nieokreślone, teoretycznie „nie ma możliwości” zdefiniowania iteracji innej niż „losowa”. W przypadku drzewa istnieje wiele „dobrych” (nieprzypadkowych) sposobów. Ale znowu twój punkt jest ważny.
alfC
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.